nx/e2e/nx-run/src/affected-graph.test.ts
Emily Xiong 40d66ec715
chore(core): remove readProjectsConfigurations from workspaces (#18329)
Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
2023-08-15 14:48:40 -04:00

640 lines
20 KiB
TypeScript

import type { NxJsonConfiguration } from '@nx/devkit';
import {
getPackageManagerCommand,
isNotWindows,
newProject,
readFile,
readJson,
cleanupProject,
runCLI,
runCLIAsync,
runCommand,
uniq,
updateFile,
updateProjectConfig,
checkFilesExist,
isWindows,
fileExists,
removeFile,
readResolvedConfiguration,
} from '@nx/e2e/utils';
describe('Nx Affected and Graph Tests', () => {
let proj: string;
beforeAll(() => (proj = newProject()));
afterAll(() => cleanupProject());
describe('affected:*', () => {
it('should print, build, and test affected apps', async () => {
process.env.CI = 'true';
const myapp = uniq('myapp');
const myapp2 = uniq('myapp2');
const mylib = uniq('mylib');
const mylib2 = uniq('mylib2');
const mypublishablelib = uniq('mypublishablelib');
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`generate @nx/web:app ${myapp2}`);
runCLI(`generate @nx/js:lib ${mylib}`);
runCLI(`generate @nx/js:lib ${mylib2}`);
runCLI(
`generate @nx/js:lib ${mypublishablelib} --publishable --importPath=@${proj}/${mypublishablelib} --tags=ui`
);
updateFile(
`apps/${myapp}/src/app/app.element.spec.ts`,
`
import * as x from '@${proj}/${mylib}';
describe('sample test', () => {
it('should test', () => {
expect(1).toEqual(1);
});
});
`
);
updateFile(
`libs/${mypublishablelib}/src/lib/${mypublishablelib}.spec.ts`,
`
import * as x from '@${proj}/${mylib}';
describe('sample test', () => {
it('should test', () => {
expect(1).toEqual(1);
});
});
`
);
const affectedApps = runCLI(
`show projects --affected --files="libs/${mylib}/src/index.ts"`
);
expect(affectedApps).toContain(myapp);
expect(affectedApps).not.toContain(myapp2);
const implicitlyAffectedApps = runCLI(
'show projects --affected --files="tsconfig.base.json"'
);
expect(implicitlyAffectedApps).toContain(myapp);
expect(implicitlyAffectedApps).toContain(myapp2);
const noAffectedApps = runCLI(
'show projects --affected projects --files="README.md"'
);
expect(noAffectedApps).not.toContain(myapp);
expect(noAffectedApps).not.toContain(myapp2);
const affectedLibs = runCLI(
`show projects --affected --files="libs/${mylib}/src/index.ts"`
);
expect(affectedLibs).toContain(mypublishablelib);
expect(affectedLibs).toContain(mylib);
expect(affectedLibs).not.toContain(mylib2);
const implicitlyAffectedLibs = runCLI(
'show projects --affected --files="tsconfig.base.json"'
);
expect(implicitlyAffectedLibs).toContain(mypublishablelib);
expect(implicitlyAffectedLibs).toContain(mylib);
expect(implicitlyAffectedLibs).toContain(mylib2);
const noAffectedLibsNonExistentFile = runCLI(
'show projects --affected --files="tsconfig.json"'
);
expect(noAffectedLibsNonExistentFile).not.toContain(mypublishablelib);
expect(noAffectedLibsNonExistentFile).not.toContain(mylib);
expect(noAffectedLibsNonExistentFile).not.toContain(mylib2);
const noAffectedLibs = runCLI(
'show projects --affected --files="README.md"'
);
expect(noAffectedLibs).not.toContain(mypublishablelib);
expect(noAffectedLibs).not.toContain(mylib);
expect(noAffectedLibs).not.toContain(mylib2);
// build
const build = runCLI(
`affected:build --files="libs/${mylib}/src/index.ts" --parallel`
);
expect(build).toContain(`Running target build for 3 projects:`);
expect(build).toContain(`- ${myapp}`);
expect(build).toContain(`- ${mypublishablelib}`);
expect(build).not.toContain('is not registered with the build command');
expect(build).toContain('Successfully ran target build');
const buildExcluded = runCLI(
`affected:build --files="libs/${mylib}/src/index.ts" --exclude=${myapp}`
);
expect(buildExcluded).toContain(`Running target build for 2 projects:`);
expect(buildExcluded).toContain(`- ${mypublishablelib}`);
const buildExcludedByTag = runCLI(
`affected:build --files="libs/${mylib}/src/index.ts" --exclude=tag:ui`
);
expect(buildExcludedByTag).toContain(
`Running target build for 2 projects:`
);
expect(buildExcludedByTag).not.toContain(`- ${mypublishablelib}`);
// test
updateFile(
`apps/${myapp}/src/app/app.element.spec.ts`,
readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace(
'.toEqual(1)',
'.toEqual(2)'
)
);
const failedTests = runCLI(
`affected:test --files="libs/${mylib}/src/index.ts"`,
{ silenceError: true }
);
expect(failedTests).toContain(mylib);
expect(failedTests).toContain(myapp);
expect(failedTests).toContain(mypublishablelib);
expect(failedTests).toContain(`Failed tasks:`);
// Fix failing Unit Test
updateFile(
`apps/${myapp}/src/app/app.element.spec.ts`,
readFile(`apps/${myapp}/src/app/app.element.spec.ts`).replace(
'.toEqual(2)',
'.toEqual(1)'
)
);
}, 1000000);
});
describe('affected (with git)', () => {
let myapp;
let myapp2;
let mylib;
beforeEach(() => {
myapp = uniq('myapp');
myapp2 = uniq('myapp');
mylib = uniq('mylib');
const nxJson: NxJsonConfiguration = readJson('nx.json');
updateFile('nx.json', JSON.stringify(nxJson));
runCommand(`git init`);
runCommand(`git config user.email "test@test.com"`);
runCommand(`git config user.name "Test"`);
runCommand(`git config commit.gpgsign false`);
try {
runCommand(
`git add . && git commit -am "initial commit" && git checkout -b main`
);
} catch (e) {}
});
function generateAll() {
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`generate @nx/web:app ${myapp2}`);
runCLI(`generate @nx/js:lib ${mylib}`);
runCommand(`git add . && git commit -am "add all"`);
}
it('should not affect other projects by generating a new project', () => {
// TODO: investigate why affected gives different results on windows
if (isNotWindows()) {
runCLI(`generate @nx/web:app ${myapp}`);
expect(runCLI('print-affected --select projects')).toContain(myapp);
runCommand(`git add . && git commit -am "add ${myapp}"`);
runCLI(`generate @nx/web:app ${myapp2}`);
let output = runCLI('print-affected --select projects');
expect(output).not.toContain(myapp);
expect(output).toContain(myapp2);
runCommand(`git add . && git commit -am "add ${myapp2}"`);
runCLI(`generate @nx/js:lib ${mylib}`);
output = runCLI('print-affected --select projects');
expect(output).not.toContain(myapp);
expect(output).not.toContain(myapp2);
expect(output).toContain(mylib);
}
}, 1000000);
it('should detect changes to projects based on tags changes', async () => {
// TODO: investigate why affected gives different results on windows
if (isNotWindows()) {
generateAll();
await updateProjectConfig(myapp, (config) => ({
...config,
tags: ['tag'],
}));
const output = runCLI('print-affected --select projects');
expect(output).toContain(myapp);
expect(output).not.toContain(myapp2);
expect(output).not.toContain(mylib);
}
});
it('should affect all projects by removing projects', async () => {
generateAll();
const root = (await readResolvedConfiguration())[mylib].root;
removeFile(root);
const output = runCLI('print-affected --select projects');
expect(output).toContain(myapp);
expect(output).toContain(myapp2);
expect(output).not.toContain(mylib);
});
it('should detect changes to implicitly dependant projects', async () => {
generateAll();
await updateProjectConfig(myapp, (config) => ({
...config,
implicitDependencies: ['*', `!${myapp2}`],
}));
runCommand('git commit -m "setup test"');
updateFile(`libs/${mylib}/index.html`, '<html></html>');
const output = runCLI('print-affected --select projects');
expect(output).toContain(myapp);
expect(output).not.toContain(myapp2);
expect(output).toContain(mylib);
// Clear implicit deps to not interfere with other tests.
await updateProjectConfig(myapp, (config) => ({
...config,
implicitDependencies: [],
}));
});
it('should handle file renames', () => {
generateAll();
// Move file
updateFile(
`apps/${myapp2}/src/index.html`,
readFile(`apps/${myapp}/src/index.html`)
);
removeFile(`apps/${myapp}/src/index.html`);
const affectedProjects = runCLI(
'print-affected --uncommitted --select projects'
)
.replace(
/.*nx print-affected --uncommitted --select projects( --verbose)?\n/,
''
)
.split(', ');
expect(affectedProjects).toContain(myapp);
expect(affectedProjects).toContain(myapp2);
});
});
describe('graph', () => {
let myapp: string;
let myapp2: string;
let myapp3: string;
let myappE2e: string;
let myapp2E2e: string;
let myapp3E2e: string;
let mylib: string;
let mylib2: string;
beforeAll(() => {
myapp = uniq('myapp');
myapp2 = uniq('myapp2');
myapp3 = uniq('myapp3');
myappE2e = `${myapp}-e2e`;
myapp2E2e = `${myapp2}-e2e`;
myapp3E2e = `${myapp3}-e2e`;
mylib = uniq('mylib');
mylib2 = uniq('mylib2');
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`generate @nx/web:app ${myapp2}`);
runCLI(`generate @nx/web:app ${myapp3}`);
runCLI(`generate @nx/js:lib ${mylib}`);
runCLI(`generate @nx/js:lib ${mylib2}`);
runCommand(`git init`);
runCommand(`git config user.email "test@test.com"`);
runCommand(`git config user.name "Test"`);
runCommand(`git config commit.gpgsign false`);
runCommand(
`git add . && git commit -am "initial commit" && git checkout -b main`
);
updateFile(
`apps/${myapp}/src/main.ts`,
`
import '@${proj}/${mylib}';
const s = {loadChildren: '@${proj}/${mylib2}'};
`
);
updateFile(
`apps/${myapp2}/src/app/app.element.spec.ts`,
`import '@${proj}/${mylib}';`
);
updateFile(
`libs/${mylib}/src/${mylib}.spec.ts`,
`import '@${proj}/${mylib2}';`
);
});
it('graph should output json to file', () => {
runCLI(`graph --file=project-graph.json`);
expect(() => checkFilesExist('project-graph.json')).not.toThrow();
const jsonFileContents = readJson('project-graph.json');
expect(jsonFileContents.graph.dependencies).toEqual(
expect.objectContaining({
[myapp3E2e]: [
{
source: myapp3E2e,
target: myapp3,
type: 'implicit',
},
],
[myapp2]: [
{
source: myapp2,
target: mylib,
type: 'static',
},
],
[myapp2E2e]: [
{
source: myapp2E2e,
target: myapp2,
type: 'implicit',
},
],
[mylib]: [
{
source: mylib,
target: mylib2,
type: 'static',
},
],
[mylib2]: [],
[myapp]: [
{
source: myapp,
target: mylib,
type: 'static',
},
],
[myappE2e]: [
{
source: myappE2e,
target: myapp,
type: 'implicit',
},
],
[myapp3]: [],
})
);
runCLI(
`affected:graph --files="libs/${mylib}/src/index.ts" --file="project-graph.json"`
);
expect(() => checkFilesExist('project-graph.json')).not.toThrow();
const jsonFileContents2 = readJson('project-graph.json');
expect(jsonFileContents2.criticalPath).toContain(myapp);
expect(jsonFileContents2.criticalPath).toContain(myapp2);
expect(jsonFileContents2.criticalPath).toContain(mylib);
expect(jsonFileContents2.criticalPath).not.toContain(mylib2);
}, 1000000);
if (isNotWindows()) {
it('graph should output json to file by absolute path', () => {
runCLI(`graph --file=/tmp/project-graph.json`);
expect(() => checkFilesExist('/tmp/project-graph.json')).not.toThrow();
}, 1000000);
}
if (isWindows()) {
it('graph should output json to file by absolute path in Windows', () => {
runCLI(`graph --file=C:\\tmp\\project-graph.json`);
expect(fileExists('C:\\tmp\\project-graph.json')).toBeTruthy();
}, 1000000);
}
it('graph should focus requested project', () => {
runCLI(`graph --focus=${myapp} --file=project-graph.json`);
expect(() => checkFilesExist('project-graph.json')).not.toThrow();
const jsonFileContents = readJson('project-graph.json');
const projectNames = Object.keys(jsonFileContents.graph.nodes);
expect(projectNames).toContain(myapp);
expect(projectNames).toContain(mylib);
expect(projectNames).toContain(mylib2);
expect(projectNames).toContain(myappE2e);
expect(projectNames).not.toContain(myapp2);
expect(projectNames).not.toContain(myapp3);
expect(projectNames).not.toContain(myapp2E2e);
expect(projectNames).not.toContain(myapp3E2e);
}, 1000000);
it('graph should exclude requested projects', () => {
runCLI(
`graph --exclude=${myappE2e},${myapp2E2e},${myapp3E2e} --file=project-graph.json`
);
expect(() => checkFilesExist('project-graph.json')).not.toThrow();
const jsonFileContents = readJson('project-graph.json');
const projectNames = Object.keys(jsonFileContents.graph.nodes);
expect(projectNames).toContain(myapp);
expect(projectNames).toContain(mylib);
expect(projectNames).toContain(mylib2);
expect(projectNames).toContain(myapp2);
expect(projectNames).toContain(myapp3);
expect(projectNames).not.toContain(myappE2e);
expect(projectNames).not.toContain(myapp2E2e);
expect(projectNames).not.toContain(myapp3E2e);
}, 1000000);
it('graph should exclude requested projects that were included by a focus', () => {
runCLI(
`graph --focus=${myapp} --exclude=${myappE2e} --file=project-graph.json`
);
expect(() => checkFilesExist('project-graph.json')).not.toThrow();
const jsonFileContents = readJson('project-graph.json');
const projectNames = Object.keys(jsonFileContents.graph.nodes);
expect(projectNames).toContain(myapp);
expect(projectNames).toContain(mylib);
expect(projectNames).toContain(mylib2);
expect(projectNames).not.toContain(myappE2e);
expect(projectNames).not.toContain(myapp2);
expect(projectNames).not.toContain(myapp3);
expect(projectNames).not.toContain(myapp2E2e);
expect(projectNames).not.toContain(myapp3E2e);
}, 1000000);
it('graph should output a deployable static website in an html file accompanied by a folder with static assets', () => {
runCLI(`graph --file=project-graph.html`);
expect(() => checkFilesExist('project-graph.html')).not.toThrow();
expect(() => checkFilesExist('static/styles.css')).not.toThrow();
expect(() => checkFilesExist('static/runtime.js')).not.toThrow();
expect(() => checkFilesExist('static/main.js')).not.toThrow();
expect(() => checkFilesExist('static/environment.js')).not.toThrow();
const environmentJs = readFile('static/environment.js');
expect(environmentJs).toContain('window.projectGraphResponse');
expect(environmentJs).toContain('"affected":[]');
});
it('graph should output valid json when stdout is specified', () => {
const result = runCLI(`affected -t build --graph stdout`);
let model;
expect(() => (model = JSON.parse(result))).not.toThrow();
expect(model).toHaveProperty('graph');
expect(model).toHaveProperty('tasks');
});
it('should include affected projects in environment file', () => {
runCLI(`graph --affected --file=project-graph.html`);
const environmentJs = readFile('static/environment.js');
const affectedProjects = environmentJs
.match(/"affected":\[(.*?)\]/)[1]
?.split(',');
expect(affectedProjects).toContain(`"${myapp}"`);
expect(affectedProjects).toContain(`"${myappE2e}"`);
expect(affectedProjects).toContain(`"${myapp2}"`);
expect(affectedProjects).toContain(`"${myapp2E2e}"`);
expect(affectedProjects).toContain(`"${mylib}"`);
});
});
});
describe('Print-affected', () => {
let proj: string;
beforeAll(() => (proj = newProject()));
afterAll(() => cleanupProject());
it('should print information about affected projects', async () => {
const myapp = uniq('myapp-a');
const myapp2 = uniq('myapp-b');
const mylib = uniq('mylib');
const mylib2 = uniq('mylib2');
const mypublishablelib = uniq('mypublishablelib');
runCLI(`generate @nx/web:app ${myapp}`);
runCLI(`generate @nx/web:app ${myapp2}`);
runCLI(`generate @nx/js:lib ${mylib}`);
runCLI(`generate @nx/js:lib ${mylib2}`);
runCLI(`generate @nx/js:lib ${mypublishablelib}`);
const app1ElementSpec = readFile(
`apps/${myapp}/src/app/app.element.spec.ts`
);
updateFile(
`apps/${myapp}/src/app/app.element.spec.ts`,
`
import "@${proj}/${mylib}";
import "@${proj}/${mypublishablelib}";
${app1ElementSpec}
`
);
const app2ElementSpec = readFile(
`apps/${myapp2}/src/app/app.element.spec.ts`
);
updateFile(
`apps/${myapp2}/src/app/app.element.spec.ts`,
`
import "@${proj}/${mylib}";
import "@${proj}/${mypublishablelib}";
${app2ElementSpec}
`
);
const resWithoutTarget = JSON.parse(
(
await runCLIAsync(
`print-affected --files=apps/${myapp}/src/app/app.element.spec.ts`,
{
silent: true,
}
)
).stdout
);
expect(resWithoutTarget.tasks).toEqual([]);
compareTwoArrays(resWithoutTarget.projects, [`${myapp}-e2e`, myapp]);
const resWithTarget = JSON.parse(
(
await runCLIAsync(
`print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test`,
{ silent: true }
)
).stdout.trim()
);
const { runNx } = getPackageManagerCommand();
expect(resWithTarget.tasks[0]).toMatchObject({
id: `${myapp}:test`,
overrides: {},
target: {
project: myapp,
target: 'test',
},
command: `${runNx} run ${myapp}:test`,
outputs: [`coverage/apps/${myapp}`],
});
compareTwoArrays(resWithTarget.projects, [myapp]);
const resWithTargetWithSelect1 = (
await runCLIAsync(
`print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test --select=projects`,
{ silent: true }
)
).stdout.trim();
compareTwoSerializedArrays(resWithTargetWithSelect1, myapp);
const resWithTargetWithSelect2 = (
await runCLIAsync(
`print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test --select="tasks.target.project"`,
{ silent: true }
)
).stdout.trim();
compareTwoSerializedArrays(resWithTargetWithSelect2, myapp);
}, 120000);
function compareTwoSerializedArrays(a: string, b: string) {
compareTwoArrays(
a.split(',').map((_) => _.trim()),
b.split(',').map((_) => _.trim())
);
}
function compareTwoArrays(a: string[], b: string[]) {
expect(a.sort((x, y) => x.localeCompare(y))).toEqual(
b.sort((x, y) => x.localeCompare(y))
);
}
});