Repository: org-arl/fjage (language: python)

Issue:
Windows CI broken
Windows CI is broken as one of the JAR attributes turns out to be `null` on Windows:
```groovy
jar {
  manifest {
    attributes 'Build-Owner': System.getenv().USER, 'Build-Timestamp': new Date().format('d-MM-yyyy_HH:mm:ss'), 'Build-Version': "${project.version}/${commit}"
  }
}
```
perhaps `System.getenv().USER`?

Temporarily disabled Windows CI to avoid it from canceling Linux CI.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index f85e37ec..c9909265 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -8,28 +8,27 @@ jobs:
     strategy:
       matrix:
         python-version: [3.5]
-        node-version: [14.x]
-        os: [ubuntu-latest]
+        node-version: [16.x]
+        os: [ubuntu-latest, windows-latest]
     steps:
     - uses: actions/checkout@v1
-    - name: Set up JDK 1.8
+    - name: Setup JDK 1.8
       uses: actions/setup-java@v1
       with:
         java-version: 1.8
-    - name: Set up Python ${{ matrix.python-version }}
+    - name: Setup Python ${{ matrix.python-version }}
       uses: actions/setup-python@v1
       with:
         python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
+    - name: Install Python dependencies
       run: |
         python -m pip install --upgrade pip
         pip install numpy nose
-    - name: Use Node.js ${{ matrix.node-version }}
+    - name: Setup Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: Setup npm CI
-      run: npm ci
-      working-directory: gateways/js
-    - name: Build with Gradle
-      run: ./gradlew test -i
+    - name: Build and Test with Gradle
+      uses: gradle/gradle-build-action@v1
+      with:
+        arguments: test -i
diff --git a/build.gradle b/build.gradle
index 4c10fdf8..cb0e7cea 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,3 +1,5 @@
+import org.apache.tools.ant.taskdefs.condition.Os
+
 plugins {
   id 'java-library'
   id 'groovy'
@@ -19,15 +21,15 @@ exec {
 }
 def commit = "$stdout".replace('\n','')
 
+// check if npm exists
+def npmcmd = Os.isFamily(Os.FAMILY_WINDOWS) ? "npm.cmd" : "npm"
 boolean npmExists = false
 try {
-  def npmproc = "npm -v".execute()
+  def npmproc = "$npmcmd -v".execute()
   npmproc.waitForOrKill(3000)
   npmExists = npmproc.exitValue() == 0
 } catch (IOException ex){}
 
-boolean isCI = System.getenv('CI') != null
-
 targetCompatibility = 1.8
 sourceCompatibility = 1.8
 
@@ -77,7 +79,10 @@ tasks.withType(Javadoc) {
 
 jar {
   manifest {
-    attributes 'Build-Owner': System.getenv().USER, 'Build-Timestamp': new Date().format('d-MM-yyyy_HH:mm:ss'), 'Build-Version': "${project.version}/${commit}"
+    attributes (
+      "Build-Owner": System.getenv().USER ?: "fjage", 
+      "Build-Timestamp": new Date().format('d-MM-yyyy_HH:mm:ss'), 
+      "Build-Version": "${project.version}/${commit}")
   }
 }
 
@@ -96,17 +101,20 @@ task lite(dependsOn: jar, type: Copy) {
 task buildjs {
   inputs.files fileTree('gateways/js/src').include('**/*.js'), "gateways/js/package.json"
   outputs.dir "gateways/js/dist"
+ 
   doLast {
     if (npmExists) {
       exec {
         workingDir 'gateways/js'
-        commandLine 'npm', isCI ? 'ci' : "install"
+        executable npmcmd
+        args System.getenv('CI') != null ? 'ci' : "install"
         errorOutput = new ByteArrayOutputStream()
         standardOutput = new ByteArrayOutputStream()
       }
       exec {
         workingDir 'gateways/js'
-        commandLine 'npm', 'run', 'build'
+        executable npmcmd
+        args 'run', 'build'
       }
     } else{
       project.logger.lifecycle('npm not found. Using cached fjage.js build')
@@ -134,7 +142,7 @@ javadoc.doLast {
 
 task jsdoc(type: Exec){
   workingDir 'gateways/js'
-  executable 'npm'
+  executable npmcmd
   args = ['run', 'docs']
 }
 
@@ -142,7 +150,8 @@ task updatexterm {
   doLast {
     exec {
       workingDir '.'
-      commandLine 'npm', 'install', 'xterm@4.13.0', 'xterm-addon-attach@0.6.0', 'xterm-addon-fit@0.5.0', 'xterm-addon-web-links@0.4.0'
+      executable npmcmd
+      args ['install', 'xterm@4.13.0', 'xterm-addon-attach@0.6.0', 'xterm-addon-fit@0.5.0', 'xterm-addon-web-links@0.4.0']
     }
     copy {
       from (['node_modules/xterm-addon-web-links/lib', 'node_modules/xterm-addon-attach/lib', 'node_modules/xterm-addon-fit/lib', 'node_modules/xterm/lib']) {
diff --git a/gateways/js/package.json b/gateways/js/package.json
index 65b2c468..5d416863 100644
--- a/gateways/js/package.json
+++ b/gateways/js/package.json
@@ -12,10 +12,11 @@
     "dist/**"
   ],
   "scripts": {
-    "build": "rm -rf dist/ && eslint src/*.js && node tools/check-version.js && rollup --silent -c rollup.config.js",
-    "test": "node test/spec/create-spec.cjs && jasmine test/spec/fjageSpec.cjs; node test/browser-test.cjs",
+    "build": "npx rimraf -rf dist/ && eslint src/*.js && node tools/check-version.js && rollup --silent -c rollup.config.js",
+    "pretest": "node test/spec/create-spec.cjs",
+    "test": "node test/run-tests.cjs",
     "docs": "documentation build src/fjage.js -f html --github --document-exported -o ../../docs/jsdoc",
-    "clean": "rm -rf dist/"
+    "clean": "npx rimraf -rf dist/"
   },
   "repository": {
     "type": "git",
diff --git a/src/main/java/org/arl/fjage/shell/ShellAgent.java b/src/main/java/org/arl/fjage/shell/ShellAgent.java
index 4cd9caf9..6673ba62 100644
--- a/src/main/java/org/arl/fjage/shell/ShellAgent.java
+++ b/src/main/java/org/arl/fjage/shell/ShellAgent.java
@@ -653,6 +653,10 @@ private void handlePutFileReq(final PutFileReq req) {
         }
       } else {
         if (contents == null && ofs == 0) {
+          if (isCache.containsKey(filename)){
+            isCache.get(filename).is.close();
+            isCache.remove(filename);
+          }
           if (f.delete()) rsp = new Message(req, Performative.AGREE);
         } else if (contents == null) {
           os = new RandomAccessFile(f, "rw");


Test patch:
diff --git a/gateways/js/test/browser-test.cjs b/gateways/js/test/browser-test.cjs
deleted file mode 100644
index 58c4ba14..00000000
--- a/gateways/js/test/browser-test.cjs
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env node
-
-const puppeteer = require('puppeteer');
-const statik = require('node-static');
-
-const port = 8000;
-
-console.log('\nSetting up local static server at http://localhost:8000/test');
-const file = new statik.Server('.');
-let server = require('http').createServer(function (request, response) {
-  request.addListener('end', function () {
-    file.serve(request, response);
-  }).resume();
-}).listen(port);
-
-
-(async () => {
-  console.log('Launching puppeteer..');
-  const browser = await puppeteer.launch();
-  const page = await browser.newPage();
-  page.on('console', msg => {
-    msg.type() == 'error' && console.log('PAGE ERR:', msg.text());
-    msg.type() == 'warning' && console.log('PAGE WARN:', msg.text());
-  });
-  await page.goto('http://127.0.0.1:'+port+'/test', {waitUntil: 'networkidle2'});
-  await page.waitForSelector('.jasmine-overall-result');
-  await page.waitForTimeout(100);
-  await browser.close();
-  console.log('Complete');
-  server.close();
-})();
\ No newline at end of file
diff --git a/gateways/js/test/run-tests.cjs b/gateways/js/test/run-tests.cjs
new file mode 100644
index 00000000..7f787344
--- /dev/null
+++ b/gateways/js/test/run-tests.cjs
@@ -0,0 +1,66 @@
+#!/usr/bin/env node
+
+const puppeteer = require('puppeteer');
+const statik = require('node-static');
+const Jasmine = require('jasmine');
+const process = require('process');
+
+
+const ip = 'localhost';
+const port = 8000;
+
+// Setup static web server for testing browser version
+console.log('Setting up local static server at http://'+ip+':'+port+'/test');
+const file = new statik.Server('.');
+let server = require('http').createServer(function (request, response) {
+  request.addListener('end', function () {
+    file.serve(request, response);
+  }).resume();
+}).listen(port);
+
+
+(async () => {
+
+  // Execute Node.js(CJS) test using Jasmine
+  let startTime = new Date();
+  console.log('Running jasmine..');
+  await runJasmine('test/spec/', ['fjageSpec.cjs']);
+  console.log('Node.js test Complete [' + (new Date() - startTime) + ' ms]');
+
+
+  // Execute Browser(MJS) test using puppteer
+  console.log('Launching puppeteer..');
+  startTime = new Date();
+  const browser = await puppeteer.launch();
+  const page = await browser.newPage();
+  page.on('console', msg => {
+    msg.type() == 'log' && console.log('PAGE LOG:', msg.text());
+    msg.type() == 'error' && console.log('PAGE ERR:', msg.text());
+    msg.type() == 'warning' && console.log('PAGE WARN:', msg.text());
+  });
+  await page.goto('http://'+ip+':'+port+'/test', {waitUntil: 'networkidle2'});
+  await page.waitForSelector('.jasmine-overall-result');
+  await page.waitForTimeout(1000);
+  await browser.close();
+  console.log('Browser test Complete [' + (new Date() - startTime) + ' ms]');
+  server.close();
+  console.log('Run Tests Complete!');
+  process.exit(0);
+})();
+
+
+// Helpers
+
+async function runJasmine (dir, specs) {
+  return new Promise((resolve) => {
+    let jasmine = new Jasmine();
+    jasmine.loadConfig({
+      spec_dir: dir,
+      spec_files: specs,
+    });
+    jasmine.onComplete(function(status) {
+      resolve(status);
+    });
+    jasmine.execute();
+  });
+}
\ No newline at end of file
diff --git a/gateways/js/test/spec/fjage.spec.js b/gateways/js/test/spec/fjage.spec.js
index 5c1a484c..015d9564 100644
--- a/gateways/js/test/spec/fjage.spec.js
+++ b/gateways/js/test/spec/fjage.spec.js
@@ -1,6 +1,6 @@
 /* global global isBrowser isJsDom isNode Performative, AgentID, Message, Gateway, MessageClass it expect describe spyOn beforeAll afterAll beforeEach jasmine*/
 
-const DIRNAME = '/tmp';
+const DIRNAME = '.';
 const FILENAME = 'fjage-test.txt';
 const TEST_STRING = 'this is a test';
 const NEW_STRING = 'new test';
@@ -109,13 +109,13 @@ describe('A Gateway', function () {
     };
     expect(createGW).not.toThrow();
     expect(gw).toBeDefined();
-    await delay(100);
+    await delay(300);
     gw.close();
   });
 
   it('should have a successfully opened the underlying connector', async function () {
     const gw = new Gateway(gwOpts);
-    await delay(200);
+    await delay(500);
     expect([1, 'open']).toContain(gw.connector.sock.readyState);
     gw.close();
   });
@@ -125,7 +125,7 @@ describe('A Gateway', function () {
     const gw = new Gateway(gwOpts);
     const gw2 = new Gateway(gwOpts);
     expect(gw).toBe(gw2);
-    await delay(100);
+    await delay(300);
     gw.close();
   });
 
@@ -133,78 +133,78 @@ describe('A Gateway', function () {
     if (!isBrowser) return;
     var gw = new Gateway(gwOpts);
     expect(gObj.fjage.gateways).toContain(gw);
-    await delay(100);
+    await delay(300);
     gw.close();
   });
 
   it('should close the socket when close is called on it', async function () {
     const gw = new Gateway(gwOpts);
-    await delay(100);
+    await delay(300);
     gw.close();
-    await delay(100);
+    await delay(300);
     expect([gw.connector.sock.CLOSED, 'closed']).toContain(gw.connector.sock.readyState);
   });
 
   it('should remove itself from global array when closed', async function () {
     if (!isBrowser) return;
     const gw = new Gateway(gwOpts);
-    await delay(100);
+    await delay(300);
     gw.close();
-    await delay(100);
+    await delay(300);
     expect(gObj.fjage.gateways.find(el => el == gw)).toBeUndefined();
   });
 
   it('should send a message over a socket', async function() {
     const shell = new AgentID('shell');
     const gw = new Gateway(gwOpts);
-    await delay(100); // delay allow for imports
+    await delay(300); // delay allow for imports
     spyOn(gw.connector.sock, 'send').and.callThrough();
     gw.connector.sock.send.calls.reset();
     const req = new ShellExecReq();
     req.recipient = shell;
     req.cmd = 'boo';
     gw.request(req);
-    await delay(100);
+    await delay(300);
     expect(gw.connector.sock.send).toHaveBeenCalled();
   });
 
   it('should send a socket message of valid fjage message structure', async function() {
     const shell = new AgentID('shell');
     const gw = new Gateway(gwOpts);
-    await delay(100); // delay allow for imports
+    await delay(300); // delay allow for imports
     spyOn(gw.connector.sock, 'send').and.callThrough();
     gw.connector.sock.send.calls.reset();
     const req = new ShellExecReq();
     req.recipient = shell;
     req.cmd = 'boo';
     gw.request(req);
-    await delay(100);
+    await delay(300);
     expect(gw.connector.sock.send).toHaveBeenCalledWith(fjageMessageChecker());
   });
 
   it('should send correct ShellExecReq of valid fjage message structure', async function() {
     const shell = new AgentID('shell');
     const gw = new Gateway(gwOpts);
-    await delay(100); // delay allow for imports
+    await delay(300); // delay allow for imports
     spyOn(gw.connector.sock, 'send').and.callThrough();
     gw.connector.sock.send.calls.reset();
     const req = new ShellExecReq();
     req.recipient = shell;
     req.cmd = 'boo';
     gw.request(req);
-    await delay(100);
+    await delay(300);
     expect(gw.connector.sock.send).toHaveBeenCalledWith(ShellExecReqChecker());
   });
 
   it('should send correct ShellExecReq of valid fjage message structure created using param constructor', async function() {
     const shell = new AgentID('shell');
     const gw = new Gateway(gwOpts);
-    await delay(100); // delay allow for imports
+    await delay(300); // delay allow for imports
     spyOn(gw.connector.sock, 'send').and.callThrough();
     gw.connector.sock.send.calls.reset();
     const req = new ShellExecReq({recipient: shell, cmd: 'boo'});
     gw.request(req);
-    await delay(100);
+    await delay(300);
     expect(gw.connector.sock.send).toHaveBeenCalledWith(ShellExecReqChecker());
   });
 
@@ -220,7 +220,7 @@ describe('A Gateway', function () {
     await delay(4000);
     expect(gw.queue.length).toBeLessThanOrEqual(128);
     if (gw.queue.length == 128){
-      var ids = gw.queue.map(m => m.id).filter( id => !!id).sort();
+      var ids = gw.queue.map(m => m.id).filter( id => !!id).sort((a,b) => a-b);
       expect(ids[ids.length-1]-ids[0]).toBe(ids.length-1);
       expect(ids[ids.length-1]).toBeGreaterThanOrEqual(128);
     }
@@ -242,7 +242,7 @@ describe('A Gateway', function () {
     }
 
     for (type = 1; type <= NMSG; type++){
-      let m = await gw.receive(m => m instanceof SendMsgRsp,1000);
+      let m = await gw.receive(m => m instanceof SendMsgRsp,2000);
       if (m && m.type){
         rxed[m.type-1] = true;
       }else{
@@ -261,7 +261,7 @@ describe('An AgentID', function () {
   });
 
   afterAll(async () => {
-    await delay(100);
+    await delay(300);
     gw.close();
   });
 
@@ -453,7 +453,7 @@ describe('Shell GetFile/PutFile', function () {
     pfr.filename = DIRNAME + '/' + FILENAME;
     const rsp = await gw.request(pfr);
     expect(rsp).not.toBeNull();
-    await delay(100);
+    await delay(300);
     gw.close();
   });
 
@@ -701,8 +701,9 @@ describe('Shell GetFile/PutFile', function () {
 });
 
 
-function sendTestStatus(status, trace, type) {
+async function sendTestStatus(status, trace, type) {
   var gw = new Gateway(gwOpts);
+  await delay(300);
   let msg = new TestCompleteNtf();
   msg.recipient = gw.agent('test');
   msg.perf = Performative.INFORM;
@@ -719,7 +720,7 @@ const autoReporter = {
     result.status == 'failed' && failedSpecs.push(result);
   },
 
-  jasmineDone: function(result, done){
+  jasmineDone: async function(result){
     var trace = '';
     for(var i = 0; i < failedSpecs.length; i++) {
       trace += 'Failed : ' + failedSpecs[i].fullName + '\n';
@@ -736,10 +737,8 @@ const autoReporter = {
         return;
       }
     }
-    sendTestStatus(result.overallStatus == 'passed', trace, testType);
-    setTimeout(() => {
-      done();
-    },500);
+    await sendTestStatus(result.overallStatus == 'passed', trace, testType);
+    await delay(500)
   }
 };
 
diff --git a/src/test/groovy/org/arl/fjage/test/fjagecTest.groovy b/src/test/groovy/org/arl/fjage/test/fjagecTest.groovy
index 2ac94b16..0dfa9c82 100644
--- a/src/test/groovy/org/arl/fjage/test/fjagecTest.groovy
+++ b/src/test/groovy/org/arl/fjage/test/fjagecTest.groovy
@@ -6,14 +6,13 @@ import org.arl.fjage.shell.*
 import org.arl.fjage.connectors.*
 import org.junit.Test
 
-import static org.junit.Assert.assertTrue
+import static org.junit.Assert.assertEquals
 
 class fjagecTest {
 
   @Test
   void fjageCTest() {
-    def testResult = false
-    def testPending = true
+    if (System.getProperty("os.name").startsWith("Windows")) return // skip on Windows
     def platform = new RealTimePlatform()
     def container = new MasterContainer(platform, 5081)
     WebServer.getInstance(8080).add("/", "/org/arl/fjage/web")
@@ -33,7 +32,7 @@ class fjagecTest {
     println "C : out = $sout \n err = $serr \n ret = $ret"
     container.shutdown()
     platform.shutdown()
-    assertTrue(ret == 0)
+    assertEquals(ret,0)
   }
 
 }
diff --git a/src/test/groovy/org/arl/fjage/test/fjagejsTest.groovy b/src/test/groovy/org/arl/fjage/test/fjagejsTest.groovy
index 943c9959..1705c3ab 100644
--- a/src/test/groovy/org/arl/fjage/test/fjagejsTest.groovy
+++ b/src/test/groovy/org/arl/fjage/test/fjagejsTest.groovy
@@ -68,11 +68,14 @@ class fjagejsTest {
       }
     })
     def ret = 0
+    def sout, serr;
     if (System.getProperty('manualJSTest') == null){
       // Run Jasmine based test in puppeteer.
       println "Running automated tests using puppeteer"
-      def proc = ['npm', '--prefix', 'gateways/js/', 'run', 'test'].execute()
-      def sout = new StringBuilder(), serr = new StringBuilder()
+      def cmd = System.properties['os.name'].toLowerCase().contains("windows") ? "npm.cmd" : "npm"
+      def proc = [cmd, '--prefix', 'gateways/js/', 'run', 'test'].execute()
+      sout = new StringBuilder() 
+      serr = new StringBuilder()
       proc.consumeProcessOutput(sout, serr)
       proc.waitFor()
       ret = proc.exitValue()
@@ -99,9 +102,9 @@ class fjagejsTest {
     println "-------------------------------------------------------------"
     container.shutdown()
     platform.shutdown()
-    assertEquals("`npm test` executed successfully?", ret, 0)
-    assertTrue("nodejs fjage.js test passed?", testRes["node"].didPass)
-    assertTrue("browser fjage.js test passed?", testRes["browser"].didPass)
+    assertEquals("npm >> " + sout.toString(), 0, ret)
+    assertTrue("JASMINE (node) >> "+testRes["node"].trace, testRes["node"].didPass)
+    assertTrue("JASMINE (browser) >> "+testRes["browser"].trace, testRes["browser"].didPass)
   }
 }
 
diff --git a/src/test/java/org/arl/fjage/test/BasicTests.java b/src/test/java/org/arl/fjage/test/BasicTests.java
index f91c744e..7bf63ba9 100644
--- a/src/test/java/org/arl/fjage/test/BasicTests.java
+++ b/src/test/java/org/arl/fjage/test/BasicTests.java
@@ -854,7 +854,7 @@ public boolean onReceive(Message msg) {
   }
 
   private class ShellTestAgent extends Agent {
-    private final String DIRNAME = "/tmp";
+    private final String DIRNAME = System.getProperty("java.io.tmpdir");
     private final String FILENAME = "fjage-test.txt";
     public boolean exec = false, put1 = false, put2 = false, put3 = false, put4 = false, get = false, get2 = false, get3 = false, get4 = false, del = false, dir = false, done = false;
     @Override