Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extract the reporter out of integrity checker #3248

Merged
4 changes: 2 additions & 2 deletions __tests__/commands/_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function run<T, R>(
args: Array<string>,
flags: Object,
name: string | { source: string, cwd: string },
checkInstalled: ?(config: Config, reporter: R, install: T) => ?Promise<void>,
checkInstalled: ?(config: Config, reporter: R, install: T, getStdout: () => string) => ?Promise<void>,
beforeInstall: ?(cwd: string) => ?Promise<void>,
): Promise<void> {
let out = '';
Expand Down Expand Up @@ -131,7 +131,7 @@ export async function run<T, R>(
const install = await factory(args, flags, config, reporter, lockfile, () => out);

if (checkInstalled) {
await checkInstalled(config, reporter, install);
await checkInstalled(config, reporter, install, () => out);
}
} catch (err) {
throw new Error(`${err && err.stack} \nConsole output:\n ${out}`);
Expand Down
90 changes: 72 additions & 18 deletions __tests__/commands/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const runCheck = buildRun.bind(
},
);

test.concurrent('--verify-tree should report wrong version ', async (): Promise<void> => {
test.concurrent('--verify-tree should report wrong version', async (): Promise<void> => {
let thrown = false;
try {
await runCheck([], {verifyTree: true}, 'verify-tree-version-mismatch');
Expand All @@ -31,7 +31,7 @@ test.concurrent('--verify-tree should report wrong version ', async (): Promise<
expect(thrown).toEqual(true);
});

test.concurrent('--verify-tree should report missing dependency ', async (): Promise<void> => {
test.concurrent('--verify-tree should report missing dependency', async (): Promise<void> => {
let thrown = false;
try {
await runCheck([], {verifyTree: true}, 'verify-tree-not-found');
Expand Down Expand Up @@ -80,6 +80,36 @@ test.concurrent('--integrity should ignore comments and whitespaces in yarn.lock
});
});

test.concurrent('--integrity should fail if integrity file is missing', async (): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'), async (config, reporter): Promise<void> => {
await fs.unlink(path.join(config.cwd, 'node_modules', '.yarn-integrity'));

let thrown = false;
try {
await checkCmd.run(config, reporter, {integrity: true}, []);
} catch (e) {
thrown = true;
}
expect(thrown).toEqual(true);
});
});

test.concurrent('--integrity should fail if integrity file is not a json', async (): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter, install, getStdout): Promise<void> => {
await fs.writeFile(path.join(config.cwd, 'node_modules', '.yarn-integrity'), 'not a json');

let thrown = false;
try {
await checkCmd.run(config, reporter, {integrity: true}, []);
} catch (e) {
thrown = true;
}
expect(thrown).toEqual(true);
expect(getStdout()).toContain('Integrity check: integrity file is not a json');
});
});

test.concurrent('--integrity should fail if yarn.lock has patterns changed', async (): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'), async (config, reporter): Promise<void> => {
let lockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
Expand All @@ -97,7 +127,8 @@ test.concurrent('--integrity should fail if yarn.lock has patterns changed', asy
});

test.concurrent('--integrity should fail if yarn.lock has new pattern', async (): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'), async (config, reporter): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter, install, getStdout): Promise<void> => {
let lockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
lockfile += `\nxtend@^4.0.0:
version "4.0.1"
Expand All @@ -111,11 +142,13 @@ test.concurrent('--integrity should fail if yarn.lock has new pattern', async ()
thrown = true;
}
expect(thrown).toEqual(true);
expect(getStdout()).toContain('Integrity check: Lock files don\'t match');
});
});

test.concurrent('--integrity should fail if yarn.lock has resolved changed', async (): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'), async (config, reporter): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter, install, getStdout): Promise<void> => {
let lockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
lockfile = lockfile.replace('https://registry.npmjs.org/left-pad/-/left-pad-1.1.1.tgz',
'https://registry.yarnpkg.com/left-pad/-/left-pad-1.1.1.tgz');
Expand All @@ -128,13 +161,14 @@ test.concurrent('--integrity should fail if yarn.lock has resolved changed', asy
thrown = true;
}
expect(thrown).toEqual(true);
expect(getStdout()).toContain('Integrity check: Lock files don\'t match');
});
});

test.concurrent('--integrity should fail if files are missing and --check-files is passed',
async (): Promise<void> => {
await runInstall({checkFiles: true}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter): Promise<void> => {
async (config, reporter, install, getStdout): Promise<void> => {
await fs.unlink(path.join(config.cwd, 'node_modules', 'left-pad', 'index.js'));

let thrown = false;
Expand All @@ -144,22 +178,23 @@ async (): Promise<void> => {
thrown = true;
}
expect(thrown).toEqual(true);
expect(getStdout()).toContain('Integrity check: Files are missing');
});
});

test.concurrent('--integrity should fail if --ignore-scripts is changed',
async (): Promise<void> => {
await runInstall({ignoreScripts: true}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter): Promise<void> => {
let thrown = false;
try {
await checkCmd.run(config, reporter, {integrity: true, ignoreScripts: false}, []);
} catch (e) {
thrown = true;
}
expect(thrown).toEqual(true);
});
});
test.concurrent('--integrity should fail if --ignore-scripts is changed', async (): Promise<void> => {
await runInstall({ignoreScripts: true}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter, install, getStdout): Promise<void> => {
let thrown = false;
try {
await checkCmd.run(config, reporter, {integrity: true, ignoreScripts: false}, []);
} catch (e) {
thrown = true;
}
expect(thrown).toEqual(true);
expect(getStdout()).toContain('Integrity check: Flags don\'t match');
});
});

test.concurrent('when switching to --check-files install should rebuild integrity file',
async (): Promise<void> => {
Expand Down Expand Up @@ -204,3 +239,22 @@ async (): Promise<void> => {

});
});

test.concurrent('--integrity should fail if integrity file have different linkedModules', async (): Promise<void> => {
await runInstall({}, path.join('..', 'check', 'integrity-lock-check'),
async (config, reporter, install, getStdout): Promise<void> => {
const integrityFilePath = path.join(config.cwd, 'node_modules', '.yarn-integrity');
const integrityFile = JSON.parse(await fs.readFile(integrityFilePath));
integrityFile.linkedModules.push('aLinkedModule');
await fs.writeFile(integrityFilePath, JSON.stringify(integrityFile, null, 2));

let thrown = false;
try {
await checkCmd.run(config, reporter, {integrity: true}, []);
} catch (e) {
thrown = true;
}
expect(thrown).toEqual(true);
expect(getStdout()).toContain('Integrity check: Linked modules don\'t match');
});
});
12 changes: 10 additions & 2 deletions src/cli/commands/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,14 @@ async function integrityHashCheck(
reporter.error(reporter.lang(msg, ...vars));
errCount++;
}
const integrityChecker = new InstallationIntegrityChecker(config, reporter);
const reasons = {
'EXPECTED_IS_NOT_A_JSON': 'integrityFailedExpectedIsNotAJSON',
'FILES_MISSING': 'integrityFailedFilesMissing',
'LOCKFILE_DONT_MATCH': 'integrityLockfilesDontMatch',
'FLAGS_DONT_MATCH': 'integrityFlagsDontMatch',
'LINKED_MODULES_DONT_MATCH': 'integrityCheckLinkedModulesDontMatch',
};
const integrityChecker = new InstallationIntegrityChecker(config);

const lockfile = await Lockfile.fromDirectory(config.cwd);
const install = new Install(flags, config, reporter, lockfile);
Expand All @@ -162,7 +169,8 @@ async function integrityHashCheck(
if (match.integrityFileMissing) {
reportError('noIntegrityFile');
}
if (!match.integrityMatches) {
if (match.integrityMatches === false) {
reporter.warn(reporter.lang(reasons[match.whyIntegrityMatchesFailed]));
reportError('integrityCheckFailed');
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class Install {

this.resolver = new PackageResolver(config, lockfile);
this.fetcher = new PackageFetcher(config, this.resolver);
this.integrityChecker = new InstallationIntegrityChecker(config, this.reporter);
this.integrityChecker = new InstallationIntegrityChecker(config);
this.compatibility = new PackageCompatibility(config, this.resolver, this.flags.ignoreEngines);
this.linker = new PackageLinker(config, this.resolver);
this.scripts = new PackageInstallScripts(config, this.resolver, this.flags.force);
Expand Down
96 changes: 44 additions & 52 deletions src/integrity-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import type Config from './config.js';
import type {LockManifest} from './lockfile/wrapper.js';
import type {RegistryNames} from './registries/index.js';
import type {Reporter} from './reporters/index.js';
import * as constants from './constants.js';
import {registryNames} from './registries/index.js';
import * as fs from './util/fs.js';
Expand Down Expand Up @@ -47,15 +46,11 @@ type IntegrityFlags = {
export default class InstallationIntegrityChecker {
constructor(
config: Config,
reporter: Reporter,
) {
this.config = config;
this.reporter = reporter;
}

config: Config;
reporter: Reporter;


/**
* Get the location of an existing integrity hash. If none exists then return the location where we should
Expand Down Expand Up @@ -167,32 +162,58 @@ export default class InstallationIntegrityChecker {
return result;
}

_compareIntegrityFiles(actual: IntegrityFile, expected: IntegrityFile): boolean {
if (!compareSortedArrays(actual.linkedModules, expected.linkedModules)) {
this.reporter.warn(this.reporter.lang('integrityCheckLinkedModulesDontMatch'));
return false;
async _getIntegrityFile(locationPath: string): Promise<?IntegrityFile> {
const expectedRaw = await fs.readFile(locationPath);
try {
return JSON.parse(expectedRaw);
} catch (e) {
// ignore JSON parsing for legacy text integrity files compatibility
}
if (!compareSortedArrays(actual.topLevelPatters, expected.topLevelPatters)) {
this.reporter.warn(this.reporter.lang('integrityPatternsDontMatch'));
return false;
return null;
}

async _compareIntegrityFiles(
actual: IntegrityFile,
expected: ?IntegrityFile,
checkFiles: boolean,
locationFolder: string): Promise<string> {
if (!expected) {
return Promise.resolve('EXPECTED_IS_NOT_A_JSON');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think flow enums would be a better fit here

}
if (!compareSortedArrays(actual.linkedModules, expected.linkedModules)) {
return Promise.resolve('LINKED_MODULES_DONT_MATCH');
}
if (!compareSortedArrays(actual.flags, expected.flags)) {
this.reporter.warn(this.reporter.lang('integrityFlagsDontMatch'));
return false;
return Promise.resolve('FLAGS_DONT_MATCH');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all these errors be a reject rather than resolve?

}
for (const key of Object.keys(actual.lockfileEntries)) {
if (actual.lockfileEntries[key] !== expected.lockfileEntries[key]) {
this.reporter.warn(this.reporter.lang('integrityLockfilesDontMatch'));
return false;
return Promise.resolve('LOCKFILE_DONT_MATCH');
}
}
for (const key of Object.keys(expected.lockfileEntries)) {
if (actual.lockfileEntries[key] !== expected.lockfileEntries[key]) {
this.reporter.warn(this.reporter.lang('integrityLockfilesDontMatch'));
return false;
return Promise.resolve('LOCKFILE_DONT_MATCH');
}
}
if (checkFiles) {
if (expected.files.length === 0) {
// edge case handling - --check-fies is passed but .yarn-integrity does not contain any files
// check and fail if there are file in node_modules after all.
const actualFiles = await this._getFilesDeep(locationFolder);
if (actualFiles.length > 0) {
return Promise.resolve('FILES_MISSING');
}
} else {
// TODO we may want to optimise this check by checking only for package.json files on very large trees
for (const file of expected.files) {
if (!await fs.exists(path.join(locationFolder, file))) {
return Promise.resolve('FILES_MISSING');
}
}
}
}
return true;
return Promise.resolve('OK');
}

async check(
Expand All @@ -214,41 +235,13 @@ export default class InstallationIntegrityChecker {
patterns,
Object.assign({}, {checkFiles: false}, flags), // don't generate files when checking, we check the files below
loc.locationFolder);
const expectedRaw = await fs.readFile(loc.locationPath);
let expected: ?IntegrityFile;
try {
expected = JSON.parse(expectedRaw);
} catch (e) {
// ignore JSON parsing for legacy text integrity files compatibility
}
let integrityMatches;
if (expected) {
integrityMatches = this._compareIntegrityFiles(actual, expected);
if (flags.checkFiles && expected.files.length === 0) {
// edge case handling - --check-fies is passed but .yarn-integrity does not contain any files
// check and fail if there are file in node_modules after all.
const actualFiles = await this._getFilesDeep(loc.locationFolder);
if (actualFiles.length > 0) {
this.reporter.warn(this.reporter.lang('integrityFailedFilesMissing'));
integrityMatches = false;
}
} else if (flags.checkFiles && expected.files.length > 0) {
// TODO we may want to optimise this check by checking only for package.json files on very large trees
for (const file of expected.files) {
if (!await fs.exists(path.join(loc.locationFolder, file))) {
this.reporter.warn(this.reporter.lang('integrityFailedFilesMissing'));
integrityMatches = false;
break;
}
}
}
} else {
integrityMatches = false;
}
const expected = await this._getIntegrityFile(loc.locationPath);
const integrityMatches = await this._compareIntegrityFiles(actual, expected, flags.checkFiles, loc.locationFolder);

return {
integrityFileMissing: false,
integrityMatches,
integrityMatches: integrityMatches === 'OK',
whyIntegrityMatchesFailed: integrityMatches,
missingPatterns,
};
}
Expand Down Expand Up @@ -295,5 +288,4 @@ export default class InstallationIntegrityChecker {
await fs.unlink(loc.locationPath);
}
}

}
2 changes: 1 addition & 1 deletion src/reporters/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ const messages = {
lockfileNotContainPattern: 'Lockfile does not contain pattern: $0',
integrityCheckFailed: 'Integrity check failed',
noIntegrityFile: 'Couldn\'t find an integrity file',
integrityFailedExpectedIsNotAJSON: 'Integrity check: integrity file is not a json',
integrityCheckLinkedModulesDontMatch: 'Integrity check: Linked modules don\'t match',
integrityPatternsDontMatch: 'Integrity check: Patterns don\'t match',
integrityFlagsDontMatch: 'Integrity check: Flags don\'t match',
integrityLockfilesDontMatch: 'Integrity check: Lock files don\'t match',
integrityFailedFilesMissing: 'Integrity check: Files are missing',
Expand Down