From a0db89c2edea09ab53f94ee16f172f6f8eb7caf1 Mon Sep 17 00:00:00 2001 From: osher Date: Mon, 22 Feb 2021 14:45:59 +0200 Subject: [PATCH 1/4] Fix Windows symlinks messing up outside dir closes #46 --- index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.js b/index.js index aecb72d..0c8477b 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const glob = promisify(require('glob')); @@ -50,6 +51,8 @@ class TestExclude { this.exclude = prepGlobPatterns([].concat(this.exclude)); + this.cwd = fs.realpathSync(this.cwd); + this.handleNegation(); } From d464b845441e36c554bc36528102185f3d38c359 Mon Sep 17 00:00:00 2001 From: osher Date: Mon, 22 Feb 2021 15:12:14 +0200 Subject: [PATCH 2/4] win32 symlimks - update tests --- test/test-exclude.js | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/test/test-exclude.js b/test/test-exclude.js index 643741c..c7158da 100644 --- a/test/test-exclude.js +++ b/test/test-exclude.js @@ -1,4 +1,5 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const t = require('tap'); @@ -59,14 +60,35 @@ t.test('does not instrument files outside cwd', t => ); if (process.platform === 'win32') { - t.test('does not instrument files on different drive (win32)', t => - testHelper(t, { - options: { - cwd: 'C:\\project' - }, - no: ['D:\\project\\foo.js'] - }) - ); + t.test('does not instrument files on different drive (win32)', t => { + const origRealPathSync = fs.realpathSync; + fs.realpathSync = s => s; + try { + testHelper(t, { + options: { + cwd: 'C:\\project' + }, + no: ['D:\\project\\foo.js'] + }) + } finally { + fs.realpathSync = origRealPathSync; + } + }); + + t.test('is not fooled by junctions (win32)', t => { + const origRealPathSync = fs.realpathSync; + fs.realpathSync = s => s.replace(/symlink/, 'truedir'); + try { + testHelper(t, { + options: { + cwd: 'C:\\project\\symlink' + }, + yes: ['C:\\project\\truedir\\foo.js'] + }) + } finally { + fs.realpathSync = origRealPathSync; + } + }); } t.test('can instrument files outside cwd if relativePath=false', t => From 96ce90ddba5801eb82ff653d3d64edfc1aaa7ff9 Mon Sep 17 00:00:00 2001 From: osher Date: Mon, 22 Feb 2021 16:05:41 +0200 Subject: [PATCH 3/4] Update test-exclude.js 3rd attempt. cloned, checked locally, but I'm on a customer's computer and for some reason github does not let me push to my own cloned repo because of some SSO mess-up, so I push it yet again via the online editor... :P --- test/test-exclude.js | 577 +++++++++++++++++++++---------------------- 1 file changed, 280 insertions(+), 297 deletions(-) diff --git a/test/test-exclude.js b/test/test-exclude.js index c7158da..fbd3283 100644 --- a/test/test-exclude.js +++ b/test/test-exclude.js @@ -1,307 +1,290 @@ -'use strict'; -const fs = require('fs'); -const path = require('path'); -const t = require('tap'); +package util; -const TestExclude = require('../'); +import com.cloudbees.groovy.cps.NonCPS; +import jenkins.NodesSharedState; +import pipelines.Base; +import util.Logger; +import util.To; -async function testHelper(t, { options, no = [], yes = [] }) { - const e = new TestExclude(options); +class Git implements Serializable { + def script; + Logger log; + Shell shell; + CommitInfo _commitInfo; - no.forEach(file => { - t.false(e.shouldInstrument(file)); - }); + Git(Shell shell, Logger log = null) { + this.script = shell.script; + this.log = log ? log.child("git") : new Logger("git", shell.script); + this.shell = shell; + } - yes.forEach(file => { - t.true(e.shouldInstrument(file)); - }); -} + Git(Base ci) { + this(new Shell(ci.script), ci.log); + } + + String checkout() { + def script = this.script; + def log = this.log.child("checkout"); + + log.info("--- checking out ---"); + script.checkout script.scm; + } -t.test('should exclude the node_modules folder by default', t => - testHelper(t, { - no: ['./banana/node_modules/cat.js', 'node_modules/cat.js'] - }) -); - -t.test('ignores ./', t => - testHelper(t, { - no: [ - './test.js', - './test.cjs', - './test.mjs', - './test.ts', - './foo.test.js', - './foo.test.cjs', - './foo.test.mjs', - './foo.test.ts' - ] - }) -); - -t.test('ignores ./test and ./tests', t => - testHelper(t, { - no: ['./test/index.js', './tests/index.js'] - }) -); - -t.test('matches files in root with **/', t => - testHelper(t, { - no: ['__tests__/**'] - }) -); - -t.test('does not instrument files outside cwd', t => - testHelper(t, { - options: { - include: ['../foo.js'] - }, - no: ['../foo.js'] - }) -); - -if (process.platform === 'win32') { - t.test('does not instrument files on different drive (win32)', t => { - const origRealPathSync = fs.realpathSync; - fs.realpathSync = s => s; - try { - testHelper(t, { - options: { - cwd: 'C:\\project' - }, - no: ['D:\\project\\foo.js'] - }) - } finally { - fs.realpathSync = origRealPathSync; + CommitInfo getCommitInfo() { + def log = this.log.child("getCommitInfo"); + if (!this._commitInfo) { + log.info("--- finding current commit info ---"); + this._commitInfo = CommitInfo.from(this.shell); } - }); - - t.test('is not fooled by junctions (win32)', t => { - const origRealPathSync = fs.realpathSync; - fs.realpathSync = s => s.replace(/symlink/, 'truedir'); - try { - testHelper(t, { - options: { - cwd: 'C:\\project\\symlink' - }, - yes: ['C:\\project\\truedir\\foo.js'] - }) - } finally { - fs.realpathSync = origRealPathSync; + + log.info("commit info", this._commitInfo.toString()); + return this._commitInfo; + } + + String getBranch() { + return this.script.env.BRANCH_NAME; + } + + + List getAuthorEmails() { + if (this.getBranch() == "master") { + def ancestor = this.getAncestorSha(); + String emailsStr = this.shell.eval("git log ${ancestor}..${this.getCommitInfo().sha} --pretty=%aE"); + List emails = [emailsStr.split('\n')].flatten().unique(); + return emails; + } + return [this.getCommitInfo().authorEmail]; + } + + String commonAncestorTo(String branch, String parent) { + def log = this.log.child("commonAncestorTo"); + + log.info("--- comparing sha of ${branch} to parent: ${parent} ---"); + def shas = this.shell.eval("git rev-parse origin/${parent} && git rev-parse origin/${branch}").split('\n'); + if (shas[0] == shas[1]) { + log.info("branch sha is the same as the master"); + return shas[0]; + } + + log.info("--- finding common ancestor commit between ${parent} and ${branch} ---"); + + def tries = 0; + def commonAncestorCommits = ["origin/${branch}", "origin/${parent}"]; + while ( + commonAncestorCommits.size() > 1 + && tries++ < 3 + ) { + commonAncestorCommits = parentCommits( + commonAncestorCommits[0], + commonAncestorCommits[commonAncestorCommits.size() - 1] + ); + log.info("try #${tries}:", commonAncestorCommits); + }; + assert commonAncestorCommits.size() == 1, 'Could not find a single ancestor after 3 tries'; + + def commonAncestorCommit = commonAncestorCommits[0]; + assert commonAncestorCommit.length(), "must have a common ancestor with master"; + + log.info("found common ancestor - ", commonAncestorCommit); + return commonAncestorCommit; + } + + List parentCommits(String branch, String parent) { + return [ + this.shell.eval( + //TRICKY: + // see: https://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git + // not the popular answer - I tested them all. + // see the one about rev-list. + [ + "git rev-list --boundary ${branch}...${parent}", + "grep \"^-\"", + "cut -c2-", + ].join(' | '), + //TRICYEnd + ) + .split("\n"), + ].flatten(); + } + + List modifiedFrom(String parentCommit, List workspaces) { + def log = this.log.child("modifiedFrom"); + log.info("--- finding modified projects since ${parentCommit} ---"); + + def str = this.shell.eval( + [ + "git diff --name-only ${parentCommit}", + "grep -P -e '^(${workspaces.join('|')})'", + "grep -v local-npm", + "cat", //TRICKY - suppresses exit-code 1 by grep when no diff files + ].join(' | '), + ); + + def modified = [str.split("\n")].flatten().findAll{ it -> 0 < it.length() }; + log.info("--- found modifications since ${parentCommit}:", modified); + + return modified; + } + + List getModifications(String branch, List workspaces) { + def lastRc = 'irrelevant'; + def ancestor = this.getAncestorSha(); + + def modifications = this.modifiedFrom(ancestor, workspaces); + + def projectPaths = modifications.collect({ file -> + //return just the `namespace/project` part + file.substring(0, file.indexOf("/", file.indexOf("/") + 1)) + }).unique(); + + this.log.info('detected modifications', [ + branch: branch, + workspaces: workspaces, + lastRcSha: lastRc, + ancestorSha: ancestor, + modifications: modifications, + projectPaths: projectPaths, + ]); + + return projectPaths; + } + + String whoIsTheAncestor(String commitA, String commitB) { + return this.shell.eval("""\ + [ "\$(git rev-list ${commitA} | grep \$(git rev-parse ${commitB}))" != "" ] && echo ${commitB}\ + || (\ + [ "\$(git rev-list ${commitB} | grep \$(git rev-parse ${commitA}))" != "" ] && echo ${commitA} \ + || echo ""\ + )""", + ); + } + + String getLastSuccessfulRcSha() { + return this.shell.eval("cat ${NodesSharedState.lastSuccessfulRcShaFilePath} || git rev-parse origin/master~1") + } + + String getAncestorSha(String branch = null) { + if (!branch) branch = this.getBranch(); + if ('master' == branch) { + def lastRc = this.getLastSuccessfulRcSha(); + return this.whoIsTheAncestor("HEAD~1", lastRc); + } else { + return this.commonAncestorTo(branch, 'master'); } - }); + } } -t.test('can instrument files outside cwd if relativePath=false', t => - testHelper(t, { - options: { - include: ['../foo.js'], - relativePath: false - }, - yes: ['../foo.js'] - }) -); - -t.test('does not instrument files in the coverage folder by default', t => - testHelper(t, { - no: ['coverage/foo.js'] - }) -); - -t.test('applies exclude rule ahead of include rule', t => - testHelper(t, { - options: { - include: ['test.js', 'foo.js'], - exclude: ['test.js'] - }, - no: ['test.js', 'banana.js'], - yes: ['foo.js'] - }) -); - -t.test('should handle gitignore-style excludes', t => - testHelper(t, { - options: { - exclude: ['dist'] - }, - no: ['dist/foo.js', 'dist/foo/bar.js'], - yes: ['src/foo.js'] - }) -); - -t.test('should handle gitignore-style includes', t => - testHelper(t, { - options: { - include: ['src'] - }, - no: ['src/foo.test.js'], - yes: ['src/foo.js', 'src/foo/bar.js'] - }) -); - -t.test("handles folder '.' in path", t => - testHelper(t, { - no: ['test/fixtures/basic/.next/dist/pages/async-props.js'] - }) -); - -t.test( - 'excludes node_modules folder, even when empty exclude group is provided', - t => - testHelper(t, { - options: { - exclude: [] - }, - no: [ - './banana/node_modules/cat.js', - 'node_modules/some/module/to/cover.js' - ], - yes: ['__tests__/a-test.js', 'src/a.test.js', 'src/foo.js'] - }) -); - -t.test( - 'allows node_modules folder to be included, if !node_modules is explicitly provided', - t => - testHelper(t, { - options: { - exclude: ['!**/node_modules/**'] - }, - yes: [ - './banana/node_modules/cat.js', - 'node_modules/some/module/to/cover.js', - '__tests__/a-test.js', - 'src/a.test.js', - 'src/foo.js' - ] - }) -); - -t.test( - 'allows specific node_modules folder to be included, if !node_modules is explicitly provided', - t => - testHelper(t, { - options: { - exclude: ['!**/node_modules/some/module/to/cover.js'] - }, - no: ['./banana/node_modules/cat.js'], - yes: [ - 'node_modules/some/module/to/cover.js', - '__tests__/a-test.js', - 'src/a.test.js', - 'src/foo.js' +class CommitInfo implements Serializable { + //--- static -------------------- + static final formatParts = ['%H', '%an', '%ae', '%aI', '%cn', '%ce', '%cI', '%B']; + static final separator = '~.~'; + static CommitInfo from(Shell shell) { + def format = formatParts.join(separator); + def parts = shell.eval("git log -1 --pretty=format:${format}").split(separator); + + //TBD: find it dynamically? + def repoUrl = "https://github.com/getjaco/ruadan_single_repo"; + + return new CommitInfo(parts, repoUrl); + } + + //--- members ------------------- + String repoUrl; + String repoName; + String authorName; + String authorEmail; + String authorDate; + String comment; + String committerName; + String committerEmail; + String committerDate; + List forcedPaths; + String sha; + String shortSha; + boolean skipSanity; + boolean skipSmokeTest; + boolean verboseStages; + + CommitInfo(parts, repoUrl) { + this.repoUrl = repoUrl; + this.repoName = repoUrl.replaceAll("\\.git", "").replaceAll(/^.*\//,""); + + def i = 0; + this.sha = parts[i++]; + this.shortSha = this.sha.substring(0, 10); + this.authorName = parts[i++]; + this.authorEmail = parts[i++]; + this.authorDate = parts[i++]; + this.committerName = parts[i++]; + this.committerEmail = parts[i++]; + this.committerDate = parts[i++]; + this.comment = parts[i++]; + + this.parseCommitDirectives(this.comment); + } + + @NonCPS + void parseCommitDirectives(comment) { + def upper = comment.toUpperCase(); + + this.verboseStages = upper.contains("@VERBOSE"); + this.skipSmokeTest = upper.contains("@NO-SMOKE-TEST"); + this.skipSanity = upper.contains("@NO-SANITY"); + + def ixForce = upper.indexOf("@FORCE"); + if (ixForce != -1) { + def terminator = comment.indexOf("@", ixForce + 6); + if (terminator == -1) { + throw new Exception([ + 'missing terminator for @FORCE directive.', + 'paths list must be terminated with a @ sign.', + 'notes:', + ' - usage: @FORCE [ [ []]] @', + ' - you may use multiple-lines', + ' - @ sign can be the opener of another directive after @FORCE', + ' when @FORCE is last - just add a @ after the paths list', + ].join('\n')); + } + + def forced = [ + comment.substring(ixForce + 6, terminator) + .split(/ +/), ] - }) -); - -t.test( - 'allows node_modules default exclusion glob to be turned off, if excludeNodeModules === false', - t => - testHelper(t, { - options: { - excludeNodeModules: false, - exclude: ['node_modules/**', '**/__test__/**'] - }, - no: [ - 'node_modules/cat.js', - './banana/node_modules/__test__/cat.test.js', - './banana/node_modules/__test__/cat.js' - ], - yes: ['./banana/node_modules/cat.js'] - }) -); - -t.test('allows negated exclude patterns', t => - testHelper(t, { - options: { - exclude: ['foo/**', '!foo/bar.js'] - }, - no: ['./foo/fizz.js'], - yes: ['./foo/bar.js'] - }) -); - -t.test('allows negated include patterns', t => - testHelper(t, { - options: { - include: ['batman/**', '!batman/robin.js'] - }, - no: ['./batman/robin.js'], - yes: ['./batman/joker.js'] - }) -); - -t.test( - 'negated exclude patterns only works for files that are covered by the `include` pattern', - t => - testHelper(t, { - options: { - include: ['index.js'], - exclude: ['!index2.js'] - }, - no: ['index2.js'], - yes: ['index.js'] - }) -); - -t.test('no extension option', t => - testHelper(t, { - options: { - extension: [] - }, - yes: ['file.js', 'package.json'] - }) -); - -t.test('handles extension option string', t => - testHelper(t, { - options: { - extension: '.js' - }, - no: ['package.json'], - yes: ['file.js'] - }) -); - -t.test('handles extension option array', t => - testHelper(t, { - options: { - extension: ['.js', '.json'] - }, - no: ['file.ts'], - yes: ['file.js', 'package.json'] - }) -); - -t.test( - 'negated exclude patterns unrelated to node_modules do not affect default node_modules exclude behavior', - t => - testHelper(t, { - options: { - exclude: ['!foo/**'] - }, - no: ['node_modules/cat.js'] - }) -); - -// see: https://github.com/istanbuljs/babel-plugin-istanbul/issues/71 -t.test('allows exclude/include rule to be a string', t => - testHelper(t, { - options: { - exclude: 'src/**/*.spec.js', - include: 'src/**' - }, - no: ['src/batman/robin/foo.spec.js'], - yes: ['src/batman/robin/foo.js'] - }) -); - -t.test('tolerates undefined exclude/include', t => - testHelper(t, { - options: { - exclude: undefined, - include: undefined - }, - no: ['test.js'], - yes: ['index.js'] - }) -); + .flatten() + .collect({ s -> s.trim() }) + .findAll({ s -> s.length() > 0 }); + + this.forcedPaths = forced.size() ? forced : ['@all']; + } + } + + String formatName() { + return this.authorName == this.commitrerName + ? this.commitrerName + : "${this.commirterName} (for ${this.authorName})"; + } + + String formatDates() { + return this.authorDate == this.commitrerDate + ? this.commitrerDate + : "Accepted at ${this.committerDate} (from ${this.authorDate})"; + } + + String toString() { + return To.string([ + sha: this.sha, + shortSha: this.shortSha, + authorName: this.authorName, + authorEmail: this.authorEmail, + authorDate: this.authorDate, + committerName: this.committerName, + committerEmail: this.committerEmail, + committerDate: this.committerDate, + comment: this.comment, + forcedPaths: this.forcedPaths, + skipSanity: this.skipSanity, + skipSmokeTest: this.skipSmokeTest, + verboseStages: this.verboseStages, + ]); + } +} From 004c62c2d3cff49868f5628a5fe2d5fbde73c988 Mon Sep 17 00:00:00 2001 From: osher Date: Mon, 22 Feb 2021 16:14:30 +0200 Subject: [PATCH 4/4] Update test-exclude.js WTF. and this time the right file! --- test/test-exclude.js | 577 ++++++++++++++++++++++--------------------- 1 file changed, 297 insertions(+), 280 deletions(-) diff --git a/test/test-exclude.js b/test/test-exclude.js index fbd3283..953eff7 100644 --- a/test/test-exclude.js +++ b/test/test-exclude.js @@ -1,290 +1,307 @@ -package util; +'use strict'; +const fs = require('fs'); +const path = require('path'); +const t = require('tap'); -import com.cloudbees.groovy.cps.NonCPS; -import jenkins.NodesSharedState; -import pipelines.Base; -import util.Logger; -import util.To; +const TestExclude = require('../'); -class Git implements Serializable { - def script; - Logger log; - Shell shell; - CommitInfo _commitInfo; +async function testHelper(t, { options, no = [], yes = [] }) { + const e = new TestExclude(options); - Git(Shell shell, Logger log = null) { - this.script = shell.script; - this.log = log ? log.child("git") : new Logger("git", shell.script); - this.shell = shell; - } + no.forEach(file => { + t.false(e.shouldInstrument(file)); + }); - Git(Base ci) { - this(new Shell(ci.script), ci.log); - } - - String checkout() { - def script = this.script; - def log = this.log.child("checkout"); - - log.info("--- checking out ---"); - script.checkout script.scm; - } - - CommitInfo getCommitInfo() { - def log = this.log.child("getCommitInfo"); - if (!this._commitInfo) { - log.info("--- finding current commit info ---"); - this._commitInfo = CommitInfo.from(this.shell); - } - - log.info("commit info", this._commitInfo.toString()); - return this._commitInfo; - } - - String getBranch() { - return this.script.env.BRANCH_NAME; - } - - - List getAuthorEmails() { - if (this.getBranch() == "master") { - def ancestor = this.getAncestorSha(); - String emailsStr = this.shell.eval("git log ${ancestor}..${this.getCommitInfo().sha} --pretty=%aE"); - List emails = [emailsStr.split('\n')].flatten().unique(); - return emails; - } - return [this.getCommitInfo().authorEmail]; - } - - String commonAncestorTo(String branch, String parent) { - def log = this.log.child("commonAncestorTo"); + yes.forEach(file => { + t.true(e.shouldInstrument(file)); + }); +} - log.info("--- comparing sha of ${branch} to parent: ${parent} ---"); - def shas = this.shell.eval("git rev-parse origin/${parent} && git rev-parse origin/${branch}").split('\n'); - if (shas[0] == shas[1]) { - log.info("branch sha is the same as the master"); - return shas[0]; +t.test('should exclude the node_modules folder by default', t => + testHelper(t, { + no: ['./banana/node_modules/cat.js', 'node_modules/cat.js'] + }) +); + +t.test('ignores ./', t => + testHelper(t, { + no: [ + './test.js', + './test.cjs', + './test.mjs', + './test.ts', + './foo.test.js', + './foo.test.cjs', + './foo.test.mjs', + './foo.test.ts' + ] + }) +); + +t.test('ignores ./test and ./tests', t => + testHelper(t, { + no: ['./test/index.js', './tests/index.js'] + }) +); + +t.test('matches files in root with **/', t => + testHelper(t, { + no: ['__tests__/**'] + }) +); + +t.test('does not instrument files outside cwd', t => + testHelper(t, { + options: { + include: ['../foo.js'] + }, + no: ['../foo.js'] + }) +); + +if (process.platform === 'win32') { + t.test('does not instrument files on different drive (win32)', async t => { + const origRealPathSync = fs.realpathSync; + fs.realpathSync = s => s; + try { + await testHelper(t, { + options: { + cwd: 'C:\\project' + }, + no: ['D:\\project\\foo.js'] + }) + } finally { + fs.realpathSync = origRealPathSync; } - - log.info("--- finding common ancestor commit between ${parent} and ${branch} ---"); - - def tries = 0; - def commonAncestorCommits = ["origin/${branch}", "origin/${parent}"]; - while ( - commonAncestorCommits.size() > 1 - && tries++ < 3 - ) { - commonAncestorCommits = parentCommits( - commonAncestorCommits[0], - commonAncestorCommits[commonAncestorCommits.size() - 1] - ); - log.info("try #${tries}:", commonAncestorCommits); - }; - assert commonAncestorCommits.size() == 1, 'Could not find a single ancestor after 3 tries'; - - def commonAncestorCommit = commonAncestorCommits[0]; - assert commonAncestorCommit.length(), "must have a common ancestor with master"; - - log.info("found common ancestor - ", commonAncestorCommit); - return commonAncestorCommit; - } - - List parentCommits(String branch, String parent) { - return [ - this.shell.eval( - //TRICKY: - // see: https://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git - // not the popular answer - I tested them all. - // see the one about rev-list. - [ - "git rev-list --boundary ${branch}...${parent}", - "grep \"^-\"", - "cut -c2-", - ].join(' | '), - //TRICYEnd - ) - .split("\n"), - ].flatten(); - } - - List modifiedFrom(String parentCommit, List workspaces) { - def log = this.log.child("modifiedFrom"); - log.info("--- finding modified projects since ${parentCommit} ---"); - - def str = this.shell.eval( - [ - "git diff --name-only ${parentCommit}", - "grep -P -e '^(${workspaces.join('|')})'", - "grep -v local-npm", - "cat", //TRICKY - suppresses exit-code 1 by grep when no diff files - ].join(' | '), - ); - - def modified = [str.split("\n")].flatten().findAll{ it -> 0 < it.length() }; - log.info("--- found modifications since ${parentCommit}:", modified); - - return modified; - } - - List getModifications(String branch, List workspaces) { - def lastRc = 'irrelevant'; - def ancestor = this.getAncestorSha(); - - def modifications = this.modifiedFrom(ancestor, workspaces); - - def projectPaths = modifications.collect({ file -> - //return just the `namespace/project` part - file.substring(0, file.indexOf("/", file.indexOf("/") + 1)) - }).unique(); - - this.log.info('detected modifications', [ - branch: branch, - workspaces: workspaces, - lastRcSha: lastRc, - ancestorSha: ancestor, - modifications: modifications, - projectPaths: projectPaths, - ]); - - return projectPaths; - } - - String whoIsTheAncestor(String commitA, String commitB) { - return this.shell.eval("""\ - [ "\$(git rev-list ${commitA} | grep \$(git rev-parse ${commitB}))" != "" ] && echo ${commitB}\ - || (\ - [ "\$(git rev-list ${commitB} | grep \$(git rev-parse ${commitA}))" != "" ] && echo ${commitA} \ - || echo ""\ - )""", - ); - } - - String getLastSuccessfulRcSha() { - return this.shell.eval("cat ${NodesSharedState.lastSuccessfulRcShaFilePath} || git rev-parse origin/master~1") - } - - String getAncestorSha(String branch = null) { - if (!branch) branch = this.getBranch(); - if ('master' == branch) { - def lastRc = this.getLastSuccessfulRcSha(); - return this.whoIsTheAncestor("HEAD~1", lastRc); - } else { - return this.commonAncestorTo(branch, 'master'); + }); + + t.test('is not fooled by junctions (win32)', async t => { + const origRealPathSync = fs.realpathSync; + fs.realpathSync = s => s.replace(/symlink/, 'truedir'); + try { + await testHelper(t, { + options: { + cwd: 'C:\\project\\symlink' + }, + yes: ['C:\\project\\truedir\\foo.js'] + }) + } finally { + fs.realpathSync = origRealPathSync; } - } + }); } -class CommitInfo implements Serializable { - //--- static -------------------- - static final formatParts = ['%H', '%an', '%ae', '%aI', '%cn', '%ce', '%cI', '%B']; - static final separator = '~.~'; - static CommitInfo from(Shell shell) { - def format = formatParts.join(separator); - def parts = shell.eval("git log -1 --pretty=format:${format}").split(separator); - - //TBD: find it dynamically? - def repoUrl = "https://github.com/getjaco/ruadan_single_repo"; - - return new CommitInfo(parts, repoUrl); - } - - //--- members ------------------- - String repoUrl; - String repoName; - String authorName; - String authorEmail; - String authorDate; - String comment; - String committerName; - String committerEmail; - String committerDate; - List forcedPaths; - String sha; - String shortSha; - boolean skipSanity; - boolean skipSmokeTest; - boolean verboseStages; - - CommitInfo(parts, repoUrl) { - this.repoUrl = repoUrl; - this.repoName = repoUrl.replaceAll("\\.git", "").replaceAll(/^.*\//,""); - - def i = 0; - this.sha = parts[i++]; - this.shortSha = this.sha.substring(0, 10); - this.authorName = parts[i++]; - this.authorEmail = parts[i++]; - this.authorDate = parts[i++]; - this.committerName = parts[i++]; - this.committerEmail = parts[i++]; - this.committerDate = parts[i++]; - this.comment = parts[i++]; - - this.parseCommitDirectives(this.comment); - } - - @NonCPS - void parseCommitDirectives(comment) { - def upper = comment.toUpperCase(); - - this.verboseStages = upper.contains("@VERBOSE"); - this.skipSmokeTest = upper.contains("@NO-SMOKE-TEST"); - this.skipSanity = upper.contains("@NO-SANITY"); - - def ixForce = upper.indexOf("@FORCE"); - if (ixForce != -1) { - def terminator = comment.indexOf("@", ixForce + 6); - if (terminator == -1) { - throw new Exception([ - 'missing terminator for @FORCE directive.', - 'paths list must be terminated with a @ sign.', - 'notes:', - ' - usage: @FORCE [ [ []]] @', - ' - you may use multiple-lines', - ' - @ sign can be the opener of another directive after @FORCE', - ' when @FORCE is last - just add a @ after the paths list', - ].join('\n')); - } - - def forced = [ - comment.substring(ixForce + 6, terminator) - .split(/ +/), +t.test('can instrument files outside cwd if relativePath=false', t => + testHelper(t, { + options: { + include: ['../foo.js'], + relativePath: false + }, + yes: ['../foo.js'] + }) +); + +t.test('does not instrument files in the coverage folder by default', t => + testHelper(t, { + no: ['coverage/foo.js'] + }) +); + +t.test('applies exclude rule ahead of include rule', t => + testHelper(t, { + options: { + include: ['test.js', 'foo.js'], + exclude: ['test.js'] + }, + no: ['test.js', 'banana.js'], + yes: ['foo.js'] + }) +); + +t.test('should handle gitignore-style excludes', t => + testHelper(t, { + options: { + exclude: ['dist'] + }, + no: ['dist/foo.js', 'dist/foo/bar.js'], + yes: ['src/foo.js'] + }) +); + +t.test('should handle gitignore-style includes', t => + testHelper(t, { + options: { + include: ['src'] + }, + no: ['src/foo.test.js'], + yes: ['src/foo.js', 'src/foo/bar.js'] + }) +); + +t.test("handles folder '.' in path", t => + testHelper(t, { + no: ['test/fixtures/basic/.next/dist/pages/async-props.js'] + }) +); + +t.test( + 'excludes node_modules folder, even when empty exclude group is provided', + t => + testHelper(t, { + options: { + exclude: [] + }, + no: [ + './banana/node_modules/cat.js', + 'node_modules/some/module/to/cover.js' + ], + yes: ['__tests__/a-test.js', 'src/a.test.js', 'src/foo.js'] + }) +); + +t.test( + 'allows node_modules folder to be included, if !node_modules is explicitly provided', + t => + testHelper(t, { + options: { + exclude: ['!**/node_modules/**'] + }, + yes: [ + './banana/node_modules/cat.js', + 'node_modules/some/module/to/cover.js', + '__tests__/a-test.js', + 'src/a.test.js', + 'src/foo.js' ] - .flatten() - .collect({ s -> s.trim() }) - .findAll({ s -> s.length() > 0 }); - - this.forcedPaths = forced.size() ? forced : ['@all']; - } - } - - String formatName() { - return this.authorName == this.commitrerName - ? this.commitrerName - : "${this.commirterName} (for ${this.authorName})"; - } - - String formatDates() { - return this.authorDate == this.commitrerDate - ? this.commitrerDate - : "Accepted at ${this.committerDate} (from ${this.authorDate})"; - } - - String toString() { - return To.string([ - sha: this.sha, - shortSha: this.shortSha, - authorName: this.authorName, - authorEmail: this.authorEmail, - authorDate: this.authorDate, - committerName: this.committerName, - committerEmail: this.committerEmail, - committerDate: this.committerDate, - comment: this.comment, - forcedPaths: this.forcedPaths, - skipSanity: this.skipSanity, - skipSmokeTest: this.skipSmokeTest, - verboseStages: this.verboseStages, - ]); - } -} + }) +); + +t.test( + 'allows specific node_modules folder to be included, if !node_modules is explicitly provided', + t => + testHelper(t, { + options: { + exclude: ['!**/node_modules/some/module/to/cover.js'] + }, + no: ['./banana/node_modules/cat.js'], + yes: [ + 'node_modules/some/module/to/cover.js', + '__tests__/a-test.js', + 'src/a.test.js', + 'src/foo.js' + ] + }) +); + +t.test( + 'allows node_modules default exclusion glob to be turned off, if excludeNodeModules === false', + t => + testHelper(t, { + options: { + excludeNodeModules: false, + exclude: ['node_modules/**', '**/__test__/**'] + }, + no: [ + 'node_modules/cat.js', + './banana/node_modules/__test__/cat.test.js', + './banana/node_modules/__test__/cat.js' + ], + yes: ['./banana/node_modules/cat.js'] + }) +); + +t.test('allows negated exclude patterns', t => + testHelper(t, { + options: { + exclude: ['foo/**', '!foo/bar.js'] + }, + no: ['./foo/fizz.js'], + yes: ['./foo/bar.js'] + }) +); + +t.test('allows negated include patterns', t => + testHelper(t, { + options: { + include: ['batman/**', '!batman/robin.js'] + }, + no: ['./batman/robin.js'], + yes: ['./batman/joker.js'] + }) +); + +t.test( + 'negated exclude patterns only works for files that are covered by the `include` pattern', + t => + testHelper(t, { + options: { + include: ['index.js'], + exclude: ['!index2.js'] + }, + no: ['index2.js'], + yes: ['index.js'] + }) +); + +t.test('no extension option', t => + testHelper(t, { + options: { + extension: [] + }, + yes: ['file.js', 'package.json'] + }) +); + +t.test('handles extension option string', t => + testHelper(t, { + options: { + extension: '.js' + }, + no: ['package.json'], + yes: ['file.js'] + }) +); + +t.test('handles extension option array', t => + testHelper(t, { + options: { + extension: ['.js', '.json'] + }, + no: ['file.ts'], + yes: ['file.js', 'package.json'] + }) +); + +t.test( + 'negated exclude patterns unrelated to node_modules do not affect default node_modules exclude behavior', + t => + testHelper(t, { + options: { + exclude: ['!foo/**'] + }, + no: ['node_modules/cat.js'] + }) +); + +// see: https://github.com/istanbuljs/babel-plugin-istanbul/issues/71 +t.test('allows exclude/include rule to be a string', t => + testHelper(t, { + options: { + exclude: 'src/**/*.spec.js', + include: 'src/**' + }, + no: ['src/batman/robin/foo.spec.js'], + yes: ['src/batman/robin/foo.js'] + }) +); + +t.test('tolerates undefined exclude/include', t => + testHelper(t, { + options: { + exclude: undefined, + include: undefined + }, + no: ['test.js'], + yes: ['index.js'] + }) +);