diff --git a/.github/workflows/master-win.yml b/.github/workflows/master-win.yml index 9fdff4af32..5951721f68 100644 --- a/.github/workflows/master-win.yml +++ b/.github/workflows/master-win.yml @@ -38,6 +38,23 @@ jobs: - name: yarn install run: yarn install --frozen-lockfile # End of yarn setup -# Tests are broken on Windows, disabled for now -# - name: test -# run: yarn lerna -- run test + + - name: lint + run: yarn lerna -- run lint + + - name: type checking and declarations + run: yarn tsc --incremental false + + - name: verify type dependencies + run: yarn lint:type-deps + + - name: test + run: yarn lerna -- run test + + - name: Discord notification + if: ${{ failure() }} + uses: Ilshidur/action-discord@0.2.0 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + with: + args: 'Windows master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}' diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 70761ad2d7..0db712e6cb 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -106,7 +106,10 @@ export function findOwnRootDir(ownDir: string) { */ export function findPaths(searchDir: string): Paths { const ownDir = findOwnDir(searchDir); - const targetDir = fs.realpathSync(process.cwd()); + // Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency + const targetDir = fs + .realpathSync(process.cwd()) + .replace(/^[a-z]:/, str => str.toUpperCase()); // Lazy load this as it will throw an error if we're not inside the Backstage repo. let ownRoot = ''; diff --git a/packages/cli/src/commands/create-plugin/createPlugin.test.ts b/packages/cli/src/commands/create-plugin/createPlugin.test.ts index 57fa33d173..72b95b072c 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.test.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.test.ts @@ -59,7 +59,7 @@ describe('createPlugin', () => { await createTemporaryPluginFolder(tempDir); await movePlugin(tempDir, pluginDir, id); await expect(fs.pathExists(pluginDir)).resolves.toBe(true); - expect(pluginDir).toMatch(`/plugins\/${id}`); + expect(pluginDir).toMatch(path.join('', 'plugins', id)); } finally { await del(tempDir, { force: true }); await del(rootDir, { force: true }); diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 72fbc6f084..3ffa8d63cf 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -39,7 +39,7 @@ const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; const tempDir = path.join(os.tmpdir(), 'remove-plugin-test'); const removeEmptyLines = (file: string): string => - file.split('\n').filter(Boolean).join('\n'); + file.split(/\r?\n/).filter(Boolean).join('\n'); const createTestPackageFile = async ( testFilePath: string, @@ -100,7 +100,7 @@ describe('removePlugin', () => { const packageFileContent = removeEmptyLines( fse.readFileSync(packageFilePath, 'utf8'), ); - expect(testFileContent === packageFileContent).toBe(true); + expect(testFileContent).toBe(packageFileContent); } finally { fse.removeSync(testFilePath); } @@ -117,7 +117,7 @@ describe('removePlugin', () => { const pluginsFileContent = removeEmptyLines( fse.readFileSync(pluginsFilePaths, 'utf8'), ); - expect(testFileContent === pluginsFileContent).toBe(true); + expect(testFileContent).toBe(pluginsFileContent); } finally { fse.removeSync(testFilePath); } @@ -138,7 +138,7 @@ describe('removePlugin', () => { 'test@gmail.com', ]); await removePluginFromCodeOwners(testFilePath, testPluginName); - expect(testFileContent === codeOwnersFileContent).toBeTruthy(); + expect(testFileContent).toBe(codeOwnersFileContent); } finally { if (fse.existsSync(testFilePath)) fse.removeSync(testFilePath); } diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 92d20ef1e7..592b8eefb3 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -37,8 +37,10 @@ }, "devDependencies": { "@types/jest": "^26.0.7", + "@types/mock-fs": "^4.10.0", "@types/node": "^12.0.0", - "@types/yup": "^0.28.2" + "@types/yup": "^0.28.2", + "mock-fs": "^4.13.0" }, "files": [ "dist" diff --git a/packages/config-loader/src/lib/resolver.test.ts b/packages/config-loader/src/lib/resolver.test.ts index c804a6cef3..9fd71f2486 100644 --- a/packages/config-loader/src/lib/resolver.test.ts +++ b/packages/config-loader/src/lib/resolver.test.ts @@ -14,46 +14,48 @@ * limitations under the License. */ -const pathExists = jest.fn(); - -jest.mock('fs-extra', () => ({ pathExists })); - +import mockFs from 'mock-fs'; import { resolveStaticConfig } from './resolver'; +function normalizePaths(paths: string[]) { + return paths.map(p => + p + .replace(/^[a-z]:/i, '') + .split('\\') + .join('/'), + ); +} + describe('resolveStaticConfig', () => { afterEach(() => { - jest.resetAllMocks(); + mockFs.restore(); }); it('should resolve no files for empty roots', async () => { + mockFs({}); const resolved = await resolveStaticConfig({ env: 'development', rootPaths: [], }); - expect(resolved).toEqual([]); - expect(pathExists).not.toHaveBeenCalled(); + expect(normalizePaths(resolved)).toEqual([]); }); it('should resolve a single app-config', async () => { - pathExists.mockImplementation(async (path: string) => - ['/repo/app-config.yaml'].includes(path), - ); + mockFs({ '/repo/app-config.yaml': '' }); const resolved = await resolveStaticConfig({ env: 'development', rootPaths: ['/repo'], }); - expect(resolved).toEqual(['/repo/app-config.yaml']); - expect(pathExists).toHaveBeenCalledTimes(4); + expect(normalizePaths(resolved)).toEqual(['/repo/app-config.yaml']); }); it('should resolve a app-configs in different directories', async () => { - pathExists.mockImplementation(async (path: string) => - ['/repo/app-config.yaml', '/repo/packages/a/app-config.yaml'].includes( - path, - ), - ); + mockFs({ + '/repo/app-config.yaml': '', + '/repo/packages/a/app-config.yaml': '', + }); const resolved = await resolveStaticConfig({ env: 'development', rootPaths: [ @@ -64,53 +66,54 @@ describe('resolveStaticConfig', () => { ], }); - expect(resolved).toEqual([ + expect(normalizePaths(resolved)).toEqual([ '/repo/app-config.yaml', '/repo/packages/a/app-config.yaml', ]); - expect(pathExists).toHaveBeenCalledTimes(16); }); it('should resolve env and local configs', async () => { - pathExists.mockImplementation(async (path: string) => - [ - '/repo/app-config.yaml', - '/repo/app-config.local.yaml', - '/repo/app-config.production.yaml', - '/repo/app-config.production.local.yaml', - '/repo/app-config.development.local.yaml', - '/repo/packages/a/app-config.development.yaml', - '/repo/packages/a/app-config.local.yaml', - ].includes(path), - ); + mockFs({ + '/repo/app-config.yaml': '', + '/repo/app-config.local.yaml': '', + '/repo/app-config.production.yaml': '', + '/repo/app-config.production.local.yaml': '', + '/repo/app-config.development.local.yaml': '', + '/repo/packages/a/app-config.development.yaml': '', + '/repo/packages/a/app-config.local.yaml': '', + }); const resolved = await resolveStaticConfig({ env: 'development', rootPaths: ['/repo', '/repo/packages/a'], }); - expect(resolved).toEqual([ + expect(normalizePaths(resolved)).toEqual([ '/repo/app-config.yaml', '/repo/app-config.local.yaml', '/repo/app-config.development.local.yaml', '/repo/packages/a/app-config.local.yaml', '/repo/packages/a/app-config.development.yaml', ]); - expect(pathExists).toHaveBeenCalledTimes(8); }); it('resolves suffixed configs in the correct order', async () => { - pathExists.mockImplementation(async () => true); + mockFs({ + '/repo/app-config.yaml': '', + '/repo/app-config.local.yaml': '', + '/repo/app-config.production.yaml': '', + '/repo/app-config.production.local.yaml': '', + }); + const resolved = await resolveStaticConfig({ env: 'production', rootPaths: ['/repo'], }); - expect(resolved).toEqual([ + expect(normalizePaths(resolved)).toEqual([ '/repo/app-config.yaml', '/repo/app-config.local.yaml', '/repo/app-config.production.yaml', '/repo/app-config.production.local.yaml', ]); - expect(pathExists).toHaveBeenCalledTimes(4); }); }); diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 4a7c9f238c..a7547ee8fb 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -15,45 +15,30 @@ */ import { loadConfig } from './loader'; - -jest.mock('fs-extra', () => { - const mockFiles: { [path in string]: string } = { - '/root/app-config.yaml': ` - app: - title: Example App - sessionKey: - $secret: - file: secrets/session-key.txt - `, - '/root/app-config.development.yaml': ` - app: - sessionKey: development-key - `, - '/root/secrets/session-key.txt': 'abc123', - '/secret-port/app-config.yaml': ` - backend: - listen: - port: - $secret: - file: secrets/port.txt - `, - '/secret-port/secrets/port.txt': '12345', - }; - - return { - async readFile(path: string) { - if (path in mockFiles) { - return mockFiles[path]; - } - throw new Error(`File not found, ${path}`); - }, - async pathExists(path: string) { - return path in mockFiles; - }, - }; -}); +import mockFs from 'mock-fs'; describe('loadConfig', () => { + beforeAll(() => { + mockFs({ + '/root/app-config.yaml': ` + app: + title: Example App + sessionKey: + $secret: + file: secrets/session-key.txt + `, + '/root/app-config.development.yaml': ` + app: + sessionKey: development-key + `, + '/root/secrets/session-key.txt': 'abc123', + }); + }); + + afterAll(() => { + mockFs.restore(); + }); + it('loads config without secrets', async () => { await expect( loadConfig({ diff --git a/packages/core/src/api-wrappers/createApp.test.tsx b/packages/core/src/api-wrappers/createApp.test.tsx index 33623b4869..a1655b88dd 100644 --- a/packages/core/src/api-wrappers/createApp.test.tsx +++ b/packages/core/src/api-wrappers/createApp.test.tsx @@ -15,21 +15,21 @@ */ import { defaultConfigLoader } from './createApp'; -import { AppConfig } from '@backstage/config'; + +(process as any).env = { NODE_ENV: 'test' }; +const anyEnv = process.env as any; describe('defaultConfigLoader', () => { afterEach(() => { - delete process.env.APP_CONFIG; + delete anyEnv.APP_CONFIG; }); it('loads static config', async () => { - Object.defineProperty(process.env, 'APP_CONFIG', { - configurable: true, - value: [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ] as AppConfig[], - }); + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + const configs = await defaultConfigLoader(); expect(configs).toEqual([ { data: { my: 'config' }, context: 'a' }, @@ -38,13 +38,11 @@ describe('defaultConfigLoader', () => { }); it('loads runtime config', async () => { - Object.defineProperty(process.env, 'APP_CONFIG', { - configurable: true, - value: [ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - ] as AppConfig[], - }); + anyEnv.APP_CONFIG = [ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + ]; + const configs = await (defaultConfigLoader as any)( '{"my":"runtime-config"}', ); @@ -62,20 +60,14 @@ describe('defaultConfigLoader', () => { }); it('fails to load invalid static config', async () => { - Object.defineProperty(process.env, 'APP_CONFIG', { - configurable: true, - value: { my: 'invalid-config' } as any, - }); + anyEnv.APP_CONFIG = { my: 'invalid-config' }; await expect(defaultConfigLoader()).rejects.toThrow( 'Static configuration has invalid format', ); }); it('fails to load bad runtime config', async () => { - Object.defineProperty(process.env, 'APP_CONFIG', { - configurable: true, - value: [{ data: { my: 'config' }, context: 'a' }] as AppConfig[], - }); + anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; await expect((defaultConfigLoader as any)('}')).rejects.toThrow( 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', diff --git a/plugins/app-backend/src/service/router.test.ts b/plugins/app-backend/src/service/router.test.ts index 4e0c27c47a..417fdc43b6 100644 --- a/plugins/app-backend/src/service/router.test.ts +++ b/plugins/app-backend/src/service/router.test.ts @@ -47,21 +47,21 @@ describe('createRouter', () => { const response = await request(app).get('/index.html'); expect(response.status).toBe(200); - expect(response.text).toBe('this is index.html\n'); + expect(response.text.trim()).toBe('this is index.html'); }); it('returns other.html', async () => { const response = await request(app).get('/other.html'); expect(response.status).toBe(200); - expect(response.text).toBe('this is other.html\n'); + expect(response.text.trim()).toBe('this is other.html'); }); it('returns index.html if missing', async () => { const response = await request(app).get('/missing.html'); expect(response.status).toBe(200); - expect(response.text).toBe('this is index.html\n'); + expect(response.text.trim()).toBe('this is index.html'); }); }); @@ -83,11 +83,11 @@ describe('createRouter with static fallback handler', () => { const response1 = await request(app).get('/static/main.txt'); expect(response1.status).toBe(200); - expect(response1.text).toBe('this is main.txt\n'); + expect(response1.text.trim()).toBe('this is main.txt'); const response2 = await request(app).get('/static/test.txt'); expect(response2.status).toBe(200); - expect(response2.text).toBe('this is test.txt'); + expect(response2.text.trim()).toBe('this is test.txt'); const response3 = await request(app).get('/static/missing.txt'); expect(response3.status).toBe(404); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index a61e5de867..4409d815b5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -96,6 +96,8 @@ describe('GitHubPreparer', () => { mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity); - expect(response).toMatch(new RegExp(/\/template\/test\/1\/2\/3$/)); + expect(response.split('\\').join('/')).toMatch( + /\/template\/test\/1\/2\/3$/, + ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.test.ts index b7e969cd47..a4441ca23f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.test.ts @@ -20,6 +20,13 @@ import Docker from 'dockerode'; import { runDockerContainer } from './helpers'; describe('helpers', () => { + if (process.platform === 'win32') { + // eslint-disable-next-line jest/no-focused-tests + it.only('should skip tests on windows', () => { + expect('test').not.toBe('run'); + }); + } + const mockDocker = new Docker() as jest.Mocked; beforeEach(() => { diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts index 1635995b9e..a77c7978da 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/dir.test.ts @@ -17,6 +17,13 @@ import { DirectoryPreparer } from './dir'; import { getVoidLogger } from '@backstage/backend-common'; import { checkoutGitRepository } from './helpers'; +function normalizePath(path: string) { + return path + .replace(/^[a-z]:/i, '') + .split('\\') + .join('/'); +} + jest.mock('./helpers', () => ({ ...jest.requireActual<{}>('./helpers'), checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch/'), @@ -47,7 +54,7 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:./our-documentation', }); - expect(await directoryPreparer.prepare(mockEntity)).toEqual( + expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( '/directory/our-documentation', ); }); @@ -61,7 +68,7 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:/our-documentation/techdocs', }); - expect(await directoryPreparer.prepare(mockEntity)).toEqual( + expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( '/our-documentation/techdocs', ); }); @@ -75,7 +82,7 @@ describe('directory preparer', () => { 'backstage.io/techdocs-ref': 'dir:./docs', }); - expect(await directoryPreparer.prepare(mockEntity)).toEqual( + expect(normalizePath(await directoryPreparer.prepare(mockEntity))).toEqual( '/tmp/backstage-repo/org/name/branch/docs', ); expect(checkoutGitRepository).toHaveBeenCalledTimes(1); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts index 99a7d3bb5c..b78203d766 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts @@ -18,6 +18,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import { GithubPreparer } from './github'; import { checkoutGithubRepository } from './helpers'; +function normalizePath(path: string) { + return path + .replace(/^[a-z]:/i, '') + .split('\\') + .join('/'); +} + jest.mock('./helpers', () => ({ ...jest.requireActual<{}>('./helpers'), checkoutGithubRepository: jest.fn( @@ -51,7 +58,7 @@ describe('github preparer', () => { const tempDocsPath = await preparer.prepare(mockEntity); expect(checkoutGithubRepository).toHaveBeenCalledTimes(1); - expect(tempDocsPath).toEqual( + expect(normalizePath(tempDocsPath)).toEqual( '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', ); }); diff --git a/yarn.lock b/yarn.lock index 57eb20b22d..72391dcef2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4496,6 +4496,13 @@ dependencies: "@types/node" "*" +"@types/mock-fs@^4.10.0": + version "4.10.0" + resolved "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.10.0.tgz#460061b186993d76856f669d5317cda8a007c24b" + integrity sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg== + dependencies: + "@types/node" "*" + "@types/morgan@^1.9.0": version "1.9.1" resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.1.tgz#6457872df95647c1dbc6b3741e8146b71ece74bf" @@ -14793,6 +14800,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdir dependencies: minimist "^1.2.5" +mock-fs@^4.13.0: + version "4.13.0" + resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-4.13.0.tgz#31c02263673ec3789f90eb7b6963676aa407a598" + integrity sha512-DD0vOdofJdoaRNtnWcrXe6RQbpHkPPmtqGq14uRX0F8ZKJ5nv89CVTYl/BZdppDxBDaV0hl75htg3abpEWlPZA== + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"