diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ff2d636 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ + +name: Unit tests + +on: + push: + branches: + - master + pull_request: + types: + - "opened" + - "synchronize" + - "reopened" +env: + NODE_VERSION: '16' + +jobs: + test: + name: Test + strategy: + fail-fast: true + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + runs-on: ${{matrix.os}} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ env.NODE_VERSION }} + - run: yarn install + - run: xvfb-run -a yarn test + if: runner.os == 'Linux' + - run: yarn test + if: runner.os != 'Linux' + diff --git a/package.json b/package.json index 35b8955..7f0a1eb 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,9 @@ "compile": "tsc -p ./ && yarn run lint", "compileDev": "tsc -p ./ && yarn run lint && webpack --mode development", "watch": "tsc -watch -p ./", - "lint": "eslint src --ext ts" + "lint": "eslint src --ext ts", + "pretest": "tsc -p ./", + "test": "node ./out/test/runTest.js" }, "devDependencies": { "@types/glob": "^7.1.3", @@ -186,12 +188,13 @@ "@types/xml2js": "^0.4.9", "@typescript-eslint/eslint-plugin": "^4.28.5", "@typescript-eslint/parser": "^4.28.5", + "@vscode/test-electron": "^2.1.3", "eslint": "^7.19.0", "glob": "^7.1.6", "mocha": "^8.2.1", "ts-loader": "^9.2.6", "tsc": "^2.0.4", - "typescript": "^4.1.3", + "typescript": "^4.6.3", "vsce": "^2.6.3", "vscode-test": "^1.5.0", "webpack": "^5.58.2", diff --git a/test/csprojReader.test.ts b/test/csprojReader.test.ts deleted file mode 100644 index 6dc56e1..0000000 --- a/test/csprojReader.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as assert from "assert"; -import CsprojReader from "../src/project/csprojReader"; - -describe("CsprojReader", () => { - it("getNamespace for valid csproj file with RootNamespace attribute in the first PropertyGroup should return Xamarin.Forms", () => { - const csproj = - '\ - \ - Xamarin.Forms\ - \ - '; - const detector = new CsprojReader(csproj); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, "Xamarin.Forms"); - }); - - it("getNamespace for valid csproj file with RootNamespace attribute in the third PropertyGroup should return System.Linq", () => { - const csproj = - '\ - \ - \ - \ - System.Linq\ - \ - '; - const detector = new CsprojReader(csproj); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, "System.Linq"); - }); - - it("getNamespace for valid csproj file without RootNamespace attribute should return undefined", () => { - const csproj = - '\ - \ - \ - '; - const detector = new CsprojReader(csproj); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, undefined); - }); - - it("getNamespace for valid csproj file without PropertyGroup attribute should return undefined", () => { - const csproj = ''; - const detector = new CsprojReader(csproj); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, undefined); - }); - - it("getNamespace for invalid csproj file should return undefined", () => { - const csproj = "lorem ipsum"; - const detector = new CsprojReader(csproj); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, undefined); - }); -}); diff --git a/test/index.ts b/test/index.ts deleted file mode 100644 index 50bae45..0000000 --- a/test/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -// -// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING -// -// This file is providing the test runner to use when running extension tests. -// By default the test runner in use is Mocha based. -// -// You can provide your own test runner if you want to override it by exporting -// a function run(testRoot: string, clb: (error:Error) => void) that the extension -// host can call to run the tests. The test runner is expected to use console.log -// to report the results back to the caller. When the tests are finished, return -// a possible error to the callback or null if none. - -var testRunner = require('vscode/lib/testrunner'); - -// You can directly control Mocha options by uncommenting the following lines -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info -testRunner.configure({ - ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - useColors: true // colored output from test results -}); - -module.exports = testRunner; \ No newline at end of file diff --git a/test/projectJsonReader.test.ts b/test/projectJsonReader.test.ts deleted file mode 100644 index 529566d..0000000 --- a/test/projectJsonReader.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as assert from "assert"; -import ProjectJsonReader from "../src/project/projectJsonReader"; - -describe("ProjectJsonReader", () => { - it("getNamespace for valid project.json with defaultNamespace attribute should return Xamarin.Forms", () => { - const json = '{"tooling": {"defaultNamespace":"Xamarin.Forms"}}'; - const detector = new ProjectJsonReader(json); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, "Xamarin.Forms"); - }); - - it("getNamespace for valid project.json without defaultNamespace attribute should return undefined", () => { - const json = '{"tooling": {}}'; - const detector = new ProjectJsonReader(json); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, undefined); - }); - - it("getNamespace for valid project.json without tooling attribute should return undefined", () => { - const json = "{}"; - const detector = new ProjectJsonReader(json); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, undefined); - }); - - it("getNamespace for invalid project.json should return undefined", () => { - const json = "lorem ipsum"; - const detector = new ProjectJsonReader(json); - - const actual = detector.getRootNamespace(); - - assert.equal(actual, undefined); - }); -}); diff --git a/test/runTest.ts b/test/runTest.ts new file mode 100644 index 0000000..b49e8c1 --- /dev/null +++ b/test/runTest.ts @@ -0,0 +1,24 @@ +import * as path from 'path'; + +import { runTests } from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test runner script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error(err); + console.error('Failed to run tests'); + process.exit(1); + } +} + +main(); diff --git a/test/suite/csprojReader.test.ts b/test/suite/csprojReader.test.ts new file mode 100644 index 0000000..0aa0dc5 --- /dev/null +++ b/test/suite/csprojReader.test.ts @@ -0,0 +1,194 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as fs from 'fs'; +import CsprojReader from '../../src/project/csprojReader'; + +const fixture_path= path.resolve(__dirname, '../suite/'); +interface Fixture { + filename: string, + csproj : string, + expected : string | undefined, +} + +suite('CsprojReader', () => { + const validTargetFramework : Array = [ + 'netcoreapp1.0', + 'netcoreapp1.1', + 'netcoreapp2.0', + 'netcoreapp2.1', + 'netcoreapp2.2', + 'netcoreapp3.0', + 'netcoreapp3.1', + 'net5.0', + 'net6.0', + ]; + + const rootNameSpacefixtures : Array = [ + { + filename: 'xamarin.csproj', + csproj: ` + + + Xamarin.Forms + + `, + expected: 'Xamarin.Forms', + }, + { + filename: 'linq.csproj', + csproj: ` + + + + System.Linq + + `, + expected: 'System.Linq', + }, + { + filename: 'empty-group.csproj', + csproj: ` + + + + `, + expected: undefined, + }, + { + filename: 'only-project-node.csproj', + csproj: '', + expected: undefined, + }, + ]; + + const targetFrameworkFixtures: Array = [ + { + filename: 'first-node.csproj', + csproj: ` + + + %PLACE_HOLDER% + + + `, + expected: '%PLACE_HOLDER%', + }, + { + filename: 'last-node.csproj', + csproj: ` + + + + %PLACE_HOLDER% + + `, + expected: '%PLACE_HOLDER%', + }, + { + filename: 'empty-group.csproj', + csproj: ` + + + + `, + expected: undefined, + }, + { + filename: 'only-project-node.csproj', + csproj: '', + expected: undefined, + }, + ]; + + const invalidCsProjFixtures : Array = [ + { + filename: 'empty.csproj', + csproj: '', + expected: undefined, + }, + { + filename: 'random-text.csproj', + csproj: 'lorem ipsum', + expected: undefined, + }, + { + filename: 'malformed-xml-1.csproj', + csproj: '<', + expected: undefined, + }, + { + filename: 'malformed-xml-2.csproj', + csproj: '<>', + expected: undefined, + }, + { + filename: 'malformed-xml-3.csproj', + csproj: '/>', + expected: undefined, + }, + { + filename: 'malformed-xml-missing-end-tag.csproj', + csproj: '', + expected: undefined, + }, + { + filename: 'malformed-xml-missing-start-tag.csproj', + csproj: '', + expected: undefined, + }, + ]; + invalidCsProjFixtures.forEach(({ filename, csproj, expected }) => { + test(`getRootNamespace from ${filename} with invalid content ${csproj} should return expected result ${expected}`, async () => { + const filePath = `${fixture_path}/${filename}`; + fs.writeFileSync(filePath, csproj); + const detector = new CsprojReader(filePath); + const actual = await detector.getRootNamespace(); + + fs.unlinkSync(filePath); + assert.strictEqual(actual, expected); + }); + test(`getTargetFramework from ${filename} with invalid content ${csproj} should return expected result ${expected}`, async () => { + const filePath = `${fixture_path}/${filename}`; + fs.writeFileSync(filePath, csproj); + const detector = new CsprojReader(filePath); + const actual = await detector.getTargetFramework(); + + fs.unlinkSync(filePath); + assert.strictEqual(actual, expected); + }); + }); + + rootNameSpacefixtures.forEach(({ filename, csproj, expected }) => { + test(`getNamespace from ${filename} with content ${csproj} should return expected result ${expected}`, async () => { + const filePath = `${fixture_path}/${filename}`; + fs.writeFileSync(filePath, csproj); + const detector = new CsprojReader(filePath); + const actual = await detector.getRootNamespace(); + + fs.unlinkSync(filePath); + assert.strictEqual(actual, expected); + }); + }); + + targetFrameworkFixtures.forEach(({ filename, csproj, expected }) => { + validTargetFramework.forEach((targetFramework, index) =>{ + test(`getTargetFramework from ${filename} with content ${csproj} should return expected result ${expected}`, async () => { + const filePath = `${fixture_path}/${index}-${filename}`; + fs.writeFileSync(filePath, csproj.replace('%PLACE_HOLDER%', targetFramework)); + const detector = new CsprojReader(filePath); + const actual = await detector.getTargetFramework(); + + fs.unlinkSync(filePath); + assert.strictEqual(actual, expected?.replace('%PLACE_HOLDER%', targetFramework)); + }); + }); + }); + + test('getFilePath return expected result',() => { + const filePath = `${fixture_path}/my-fancy-csproj-file`; + const detector = new CsprojReader(filePath); + const actual = detector.getFilePath(); + + assert.strictEqual(actual, filePath); + }); +}); diff --git a/test/suite/index.ts b/test/suite/index.ts new file mode 100644 index 0000000..2ce3229 --- /dev/null +++ b/test/suite/index.ts @@ -0,0 +1,38 @@ +import * as path from 'path'; +import * as Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); +} diff --git a/test/suite/projectJsonReader.test.ts b/test/suite/projectJsonReader.test.ts new file mode 100644 index 0000000..4268548 --- /dev/null +++ b/test/suite/projectJsonReader.test.ts @@ -0,0 +1,55 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as fs from 'fs'; +import ProjectJsonReader from '../../src/project/projectJsonReader'; + +const fixture_path= path.resolve(__dirname, '../suite/'); +interface Fixture { + filename: string, + json : string, + expected : string | undefined, +} + +suite('ProjectJsonReader', () => { + const fixtures : Array = [ + { + filename: 'xamarin.json', + json: '{"tooling": {"defaultNamespace":"Xamarin.Forms"}}', + expected: 'Xamarin.Forms', + }, + { + filename: 'empty-configuration.json', + json: '{"tooling": {}}', + expected: undefined, + }, + { + filename: 'empty-json.json', + json: '{}', + expected: undefined, + }, + { + filename: 'wrong-json.json', + json: 'lorem ipsum', + expected: undefined, + }, + ]; + fixtures.forEach(({ filename, json, expected }) => { + test(`getNamespace from ${filename} with content ${json} should return expected result ${expected}`, async () => { + const filePath = `${fixture_path}/${filename}`; + fs.writeFileSync(filePath, json); + const detector = new ProjectJsonReader(filePath); + const actual = await detector.getRootNamespace(); + + fs.unlinkSync(filePath); + assert.strictEqual(actual, expected); + }); + }); + + test('getFilePath return expected result',() => { + const filePath = `${fixture_path}/my-fancy-csproj-file`; + const detector = new ProjectJsonReader(filePath); + const actual = detector.getFilePath(); + + assert.strictEqual(actual, filePath); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 5c32be4..c1ad01e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,4 +17,4 @@ "node_modules", ".vscode-test" ] -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 1d37254..9d73178 100644 --- a/yarn.lock +++ b/yarn.lock @@ -214,6 +214,16 @@ resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== +"@vscode/test-electron@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.1.3.tgz#c66c4a29ede1f940c2fa204d269b660b0126dc7f" + integrity sha512-ps/yJ/9ToUZtR1dHfWi1mDXtep1VoyyrmGKC3UnIbScToRQvbUjyy1VMqnMEW3EpMmC3g7+pyThIPtPyCLHyow== + dependencies: + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + rimraf "^3.0.2" + unzipper "^0.10.11" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -2747,10 +2757,10 @@ typed-rest-client@^1.8.4: tunnel "0.0.6" underscore "^1.12.1" -typescript@^4.1.3: - version "4.2.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== +typescript@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" + integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6"