Skip to content

Commit

Permalink
fix(input files): support emojis in file names (#2430)
Browse files Browse the repository at this point in the history
Support emojis and other encoded characters in file names when using `git ls-files`.
  • Loading branch information
nicojs authored Aug 27, 2020
1 parent 7a151c4 commit 139f9f3
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 30 deletions.
1 change: 1 addition & 0 deletions e2e/test/mocha-javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"pretest": "rimraf \"reports\"",
"test": "stryker run",
"test:unit": "mocha",
"posttest": "mocha --no-config --require ../../tasks/ts-node-register.js verify/*.ts"
},
"author": "",
Expand Down
4 changes: 4 additions & 0 deletions e2e/test/mocha-javascript/src/🐱‍👓ninja.cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

module.exports = function ninjaCatSays(toSay) {
return `🐱‍👓 ${toSay}`;
}
10 changes: 10 additions & 0 deletions e2e/test/mocha-javascript/test/unit/ninjaCat.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const ninjaCatSays = require('../../src/🐱‍👓ninja.cat');
const { expect } = require('chai');

describe('🐱‍👓', () => {
describe('ninjaCatSays', () => {
it('should speak', () => {
expect(ninjaCatSays('ATTACK!')).eq('🐱‍👓 ATTACK!')
});
});
});
15 changes: 8 additions & 7 deletions e2e/test/mocha-javascript/verify/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ describe('Verify stryker has ran correctly', () => {

it('should report correct score', async () => {
await expectMetricsResult({

metrics: produceMetrics({
killed: 16,
mutationScore: 64,
mutationScoreBasedOnCoveredCode: 64,
killed: 18,
mutationScore: 66.67,
mutationScoreBasedOnCoveredCode: 66.67,
survived: 9,
totalCovered: 25,
totalDetected: 16,
totalMutants: 25,
totalCovered: 27,
totalDetected: 18,
totalMutants: 27,
totalUndetected: 9,
totalValid: 25
totalValid: 27
})
});
});
Expand Down
63 changes: 44 additions & 19 deletions packages/core/src/input/InputFileResolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as path from 'path';
import { promises as fs } from 'fs';

import { StringDecoder } from 'string_decoder';

import { File, StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
Expand All @@ -23,6 +25,29 @@ function toReportSourceFile(file: File): SourceFile {

const IGNORE_PATTERN_CHARACTER = '!';

/**
* When characters are represented as the octal values of its utf8 encoding
* e.g. å becomes \303\245 in git.exe output
*/
function decodeGitLsOutput(line: string) {
if (line.startsWith('"') && line.endsWith('"')) {
return line
.substr(1, line.length - 2)
.replace(/\\\\/g, '\\')
.replace(/(?:\\\d+)*/g, (octalEscapeSequence) =>
new StringDecoder('utf-8').write(
Buffer.from(
octalEscapeSequence
.split('\\')
.filter(Boolean)
.map((octal) => parseInt(octal, 8))
)
)
);
}
return line;
}

export default class InputFileResolver {
private readonly mutatePatterns: readonly string[];
private readonly filePatterns: readonly string[] | undefined;
Expand Down Expand Up @@ -103,12 +128,14 @@ export default class InputFileResolver {
const { stdout } = await childProcessAsPromised.exec(`git ls-files --others --exclude-standard --cached --exclude /${this.tempDirName}/*`, {
maxBuffer: 10 * 1000 * 1024,
});
const fileNames = stdout
const relativeFileNames = stdout
.toString()
.split('\n')
.map((line) => line.trim())
.filter((line) => line) // remove empty lines
.map((relativeFileName) => path.resolve(relativeFileName));
.filter(Boolean) // remove empty lines
.map(decodeGitLsOutput);

const fileNames = relativeFileNames.map((relativeFileName) => path.resolve(relativeFileName));
return fileNames;
} catch (error) {
throw new StrykerError(
Expand All @@ -135,21 +162,19 @@ export default class InputFileResolver {
return files.filter(notEmpty);
}

private readFile(fileName: string): Promise<File | null> {
return fs
.readFile(fileName)
.then((content: Buffer) => new File(fileName, content))
.then((file: File) => {
this.reportSourceFilesRead(file);
return file;
})
.catch((error) => {
if ((isErrnoException(error) && error.code === 'ENOENT') || error.code === 'EISDIR') {
return null; // file is deleted or a directory. This can be a valid result of the git command
} else {
// Rethrow
throw error;
}
});
private async readFile(fileName: string): Promise<File | null> {
try {
const content = await fs.readFile(fileName);
const file = new File(fileName, content);
this.reportSourceFilesRead(file);
return file;
} catch (error) {
if ((isErrnoException(error) && error.code === 'ENOENT') || error.code === 'EISDIR') {
return null; // file is deleted or a directory. This can be a valid result of the git command
} else {
// Rethrow
throw error;
}
}
}
}
24 changes: 21 additions & 3 deletions packages/core/test/unit/input/InputFileResolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as fs from 'fs';

import { File } from '@stryker-mutator/api/core';
import { SourceFile } from '@stryker-mutator/api/report';
import { testInjector, factory } from '@stryker-mutator/test-helpers';
import { testInjector, factory, assertions } from '@stryker-mutator/test-helpers';
import { createIsDirError, fileNotFoundError } from '@stryker-mutator/test-helpers/src/factory';
import { childProcessAsPromised, errorToString } from '@stryker-mutator/util';
import { expect } from 'chai';
Expand Down Expand Up @@ -33,8 +33,6 @@ describe(InputFileResolver.name, () => {
.stub(fs.promises, 'readFile')
.withArgs(sinon.match.string)
.resolves(Buffer.from('')) // fallback
.withArgs(sinon.match.string)
.resolves(Buffer.from('')) // fallback
.withArgs(sinon.match('file1'))
.resolves(Buffer.from('file 1 content'))
.withArgs(sinon.match('file2'))
Expand Down Expand Up @@ -134,6 +132,26 @@ describe(InputFileResolver.name, () => {
expect(result.files).lengthOf(0);
});

it('should decode encoded file names', async () => {
sut = createSut();
childProcessExecStub.resolves({
// \303\245 = å
// \360\237\220\261\342\200\215\360\237\221\223 = 🐱‍👓
// On linux, files are allowed to contain `\`, which is also escaped in git output
stdout: Buffer.from(`
"\\303\\245.js"
"src/\\360\\237\\220\\261\\342\\200\\215\\360\\237\\221\\223ninja.cat.js"
"a\\\\test\\\\file.js"
`),
});
const files = await sut.resolve();
assertions.expectTextFilesEqual(files.files, [
new File(path.resolve('å.js'), ''),
new File(path.resolve('src/🐱‍👓ninja.cat.js'), ''),
new File(path.resolve('a\\test\\file.js'), ''),
]);
});

describe('with mutate file expressions', () => {
it('should result in the expected mutate files', async () => {
testInjector.options.mutate = ['mute*'];
Expand Down
2 changes: 1 addition & 1 deletion packages/test-helpers/src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function expectTextFileEqual(actual: File, expected: File) {
expect(fileToJson(actual)).deep.eq(fileToJson(expected));
}

export function expectTextFilesEqual(actual: File[], expected: File[]) {
export function expectTextFilesEqual(actual: readonly File[], expected: readonly File[]) {
expect(actual.map(fileToJson)).deep.eq(expected.map(fileToJson));
}

Expand Down

0 comments on commit 139f9f3

Please sign in to comment.