diff --git a/action.yml b/action.yml index 707dbac..53e16a7 100644 --- a/action.yml +++ b/action.yml @@ -26,6 +26,9 @@ inputs: instrumentHttpPayloads: description: 'Whether Scope should instrument HTTP payloads, false by default' required: false + agentVersion: + description: 'Version of the Scope agent to use for testing, by default the latest stabl' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index ecd6d35..29d37de 100644 --- a/dist/index.js +++ b/dist/index.js @@ -48,6 +48,7 @@ module.exports = const core = __webpack_require__(310); const exec = __webpack_require__(230); +const io = __webpack_require__(954); const fetch = __webpack_require__(735); const fs = __webpack_require__(747); const semver = __webpack_require__(105); @@ -70,6 +71,12 @@ async function run() { const configuration = core.getInput('configuration') || 'Debug'; const agentVersion = core.getInput('agentVersion'); + //If project uses testplan force use of code coverage + let file_list = recFindByExt('.','xctestplan'); + for(let testPlanFile of file_list ){ + await deleteLinesContaining(testPlanFile, 'codeCoverage') + } + //Read project const workspace = await getWorkspace(); let xcodeproj = await getXCodeProj(); @@ -95,7 +102,6 @@ async function run() { console.log(`Scheme selected: ${scheme}`); //copy configfile - const configfileName = 'scopeConfig.xcconfig'; const configFilePath = scopeDir + '/' + configfileName; @@ -105,8 +111,6 @@ async function run() { } createXCConfigFile(configFilePath); - //enableCodeCoverage in xcodebuild doesn't work with test plans, configure them before - configureTestPlansForCoverage(projectParameter, scheme); //download scope await downloadLatestScope(agentVersion); @@ -297,6 +301,7 @@ async function downloadLatestScope(agentVersion) { }); const scopeURL = versions[agentVersion] || versions[currentVersion]; const scopePath = scopeDir + '/scopeAgent.zip'; + console.log(`Scope agent downloading: ${scopeURL}`); await downloadFile(scopeURL, scopePath); const extractCommand = 'ditto -x -k ' + scopePath + ' ' + scopeDir + '/scopeAgent'; @@ -382,32 +387,6 @@ async function insertEnvVariable( name, value, file, target) { } } -async function configureTestPlansForCoverage( projectParameter, scheme ) { - //Check if project is configured with test plans - let showTestPlansCommand = 'xcodebuild -showTestPlans -json ' + projectParameter + ' -scheme ' + scheme; - let auxOutput = ''; - const options = {}; - options.listeners = { - stdout: (data) => { - auxOutput += data.toString(); - } - }; - await exec.exec(showTestPlansCommand, null, options); - const showTestPlans = JSON.parse(auxOutput); - if( showTestPlans.testPlans === null ) { - return; - } - - //If uses testplan configure to use code coverage - let file_list = recFindByExt('.','xctestplan'); - for(let testPlanFile of file_list ){ - let rawdata = fs.readFileSync(testPlanFile); - let testPlan = JSON.parse(rawdata); - testPlan.defaultOptions.codeCoverage = true; - fs.writeFileSync(testPlanFile, JSON.stringify(testPlan)); - } -} - function recFindByExt(base,ext,files,result) { files = files || fs.readdirSync(base); @@ -432,6 +411,36 @@ function recFindByExt(base,ext,files,result) return result } +async function deleteLinesContaining( file, match ) { + let newName = file + '_old'; + await io.mv(file, newName ); + fs.readFile(newName, {encoding: 'utf-8'}, function(err, data) { + if (err) throw error; + + let dataArray = data.split('\n'); // convert file data in an array + const searchKeyword = match; // we are looking for a line, contains, key word 'user1' in the file + let lastIndex = -1; // let say, we have not found the keyword + + for (let index=0; index { + if (err) throw err; + console.log ('Successfully updated the file data'); + }); + + }); +} run(); @@ -2642,6 +2651,208 @@ class ExecState extends events.EventEmitter { /***/ }), +/***/ 223: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +const assert_1 = __webpack_require__(357); +const fs = __webpack_require__(747); +const path = __webpack_require__(622); +_a = fs.promises, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; +exports.IS_WINDOWS = process.platform === 'win32'; +function exists(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield exports.stat(fsPath); + } + catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + return true; + }); +} +exports.exists = exists; +function isDirectory(fsPath, useStat = false) { + return __awaiter(this, void 0, void 0, function* () { + const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); + return stats.isDirectory(); + }); +} +exports.isDirectory = isDirectory; +/** + * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: + * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). + */ +function isRooted(p) { + p = normalizeSeparators(p); + if (!p) { + throw new Error('isRooted() parameter "p" cannot be empty'); + } + if (exports.IS_WINDOWS) { + return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + ); // e.g. C: or C:\hello + } + return p.startsWith('/'); +} +exports.isRooted = isRooted; +/** + * Recursively create a directory at `fsPath`. + * + * This implementation is optimistic, meaning it attempts to create the full + * path first, and backs up the path stack from there. + * + * @param fsPath The path to create + * @param maxDepth The maximum recursion depth + * @param depth The current recursion depth + */ +function mkdirP(fsPath, maxDepth = 1000, depth = 1) { + return __awaiter(this, void 0, void 0, function* () { + assert_1.ok(fsPath, 'a path argument must be provided'); + fsPath = path.resolve(fsPath); + if (depth >= maxDepth) + return exports.mkdir(fsPath); + try { + yield exports.mkdir(fsPath); + return; + } + catch (err) { + switch (err.code) { + case 'ENOENT': { + yield mkdirP(path.dirname(fsPath), maxDepth, depth + 1); + yield exports.mkdir(fsPath); + return; + } + default: { + let stats; + try { + stats = yield exports.stat(fsPath); + } + catch (err2) { + throw err; + } + if (!stats.isDirectory()) + throw err; + } + } + } + }); +} +exports.mkdirP = mkdirP; +/** + * Best effort attempt to determine whether a file exists and is executable. + * @param filePath file path to check + * @param extensions additional file extensions to try + * @return if file exists and is executable, returns the file path. otherwise empty string. + */ +function tryGetExecutablePath(filePath, extensions) { + return __awaiter(this, void 0, void 0, function* () { + let stats = undefined; + try { + // test file exists + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // on Windows, test for valid extension + const upperExt = path.extname(filePath).toUpperCase(); + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath; + } + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + // try each extension + const originalFilePath = filePath; + for (const extension of extensions) { + filePath = originalFilePath + extension; + stats = undefined; + try { + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // preserve the case of the actual file (since an extension was appended) + try { + const directory = path.dirname(filePath); + const upperName = path.basename(filePath).toUpperCase(); + for (const actualName of yield exports.readdir(directory)) { + if (upperName === actualName.toUpperCase()) { + filePath = path.join(directory, actualName); + break; + } + } + } + catch (err) { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); + } + return filePath; + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + } + return ''; + }); +} +exports.tryGetExecutablePath = tryGetExecutablePath; +function normalizeSeparators(p) { + p = p || ''; + if (exports.IS_WINDOWS) { + // convert slashes on Windows + p = p.replace(/\//g, '\\'); + // remove redundant slashes + return p.replace(/\\\\+/g, '\\'); + } + // remove redundant slashes + return p.replace(/\/\/+/g, '/'); +} +// on Mac/Linux, test the execute bit +// R W X R W X R W X +// 256 128 64 32 16 8 4 2 1 +function isUnixExecutable(stats) { + return ((stats.mode & 1) > 0 || + ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || + ((stats.mode & 64) > 0 && stats.uid === process.getuid())); +} +//# sourceMappingURL=io-util.js.map + +/***/ }), + /***/ 230: /***/ (function(__unusedmodule, exports, __webpack_require__) { @@ -2888,6 +3099,13 @@ exports.getState = getState; /***/ }), +/***/ 357: +/***/ (function(module) { + +module.exports = require("assert"); + +/***/ }), + /***/ 413: /***/ (function(module) { @@ -2916,6 +3134,13 @@ module.exports = require("path"); /***/ }), +/***/ 669: +/***/ (function(module) { + +module.exports = require("util"); + +/***/ }), + /***/ 735: /***/ (function(module, exports, __webpack_require__) { @@ -4587,6 +4812,303 @@ module.exports = require("url"); /***/ }), +/***/ 954: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const childProcess = __webpack_require__(129); +const path = __webpack_require__(622); +const util_1 = __webpack_require__(669); +const ioUtil = __webpack_require__(223); +const exec = util_1.promisify(childProcess.exec); +/** + * Copies a file or folder. + * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +function cp(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + const { force, recursive } = readCopyOptions(options); + const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; + // Dest is an existing file, but not forcing + if (destStat && destStat.isFile() && !force) { + return; + } + // If dest is an existing directory, should copy inside. + const newDest = destStat && destStat.isDirectory() + ? path.join(dest, path.basename(source)) + : dest; + if (!(yield ioUtil.exists(source))) { + throw new Error(`no such file or directory: ${source}`); + } + const sourceStat = yield ioUtil.stat(source); + if (sourceStat.isDirectory()) { + if (!recursive) { + throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); + } + else { + yield cpDirRecursive(source, newDest, 0, force); + } + } + else { + if (path.relative(source, newDest) === '') { + // a file cannot be copied to itself + throw new Error(`'${newDest}' and '${source}' are the same file`); + } + yield copyFile(source, newDest, force); + } + }); +} +exports.cp = cp; +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See MoveOptions. + */ +function mv(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + if (yield ioUtil.exists(dest)) { + let destExists = true; + if (yield ioUtil.isDirectory(dest)) { + // If dest is directory copy src into dest + dest = path.join(dest, path.basename(source)); + destExists = yield ioUtil.exists(dest); + } + if (destExists) { + if (options.force == null || options.force) { + yield rmRF(dest); + } + else { + throw new Error('Destination already exists'); + } + } + } + yield mkdirP(path.dirname(dest)); + yield ioUtil.rename(source, dest); + }); +} +exports.mv = mv; +/** + * Remove a path recursively with force + * + * @param inputPath path to remove + */ +function rmRF(inputPath) { + return __awaiter(this, void 0, void 0, function* () { + if (ioUtil.IS_WINDOWS) { + // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another + // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. + try { + if (yield ioUtil.isDirectory(inputPath, true)) { + yield exec(`rd /s /q "${inputPath}"`); + } + else { + yield exec(`del /f /a "${inputPath}"`); + } + } + catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') + throw err; + } + // Shelling out fails to remove a symlink folder with missing source, this unlink catches that + try { + yield ioUtil.unlink(inputPath); + } + catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') + throw err; + } + } + else { + let isDir = false; + try { + isDir = yield ioUtil.isDirectory(inputPath); + } + catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') + throw err; + return; + } + if (isDir) { + yield exec(`rm -rf "${inputPath}"`); + } + else { + yield ioUtil.unlink(inputPath); + } + } + }); +} +exports.rmRF = rmRF; +/** + * Make a directory. Creates the full path with folders in between + * Will throw if it fails + * + * @param fsPath path to create + * @returns Promise + */ +function mkdirP(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + yield ioUtil.mkdirP(fsPath); + }); +} +exports.mkdirP = mkdirP; +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * If you check and the tool does not exist, it will throw. + * + * @param tool name of the tool + * @param check whether to check if tool exists + * @returns Promise path to tool + */ +function which(tool, check) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // recursive when check=true + if (check) { + const result = yield which(tool, false); + if (!result) { + if (ioUtil.IS_WINDOWS) { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); + } + else { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); + } + } + } + try { + // build the list of extensions to try + const extensions = []; + if (ioUtil.IS_WINDOWS && process.env.PATHEXT) { + for (const extension of process.env.PATHEXT.split(path.delimiter)) { + if (extension) { + extensions.push(extension); + } + } + } + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); + if (filePath) { + return filePath; + } + return ''; + } + // if any path separators, return empty + if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) { + return ''; + } + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the toolkit should strive for consistency + // across platforms. + const directories = []; + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p); + } + } + } + // return the first match + for (const directory of directories) { + const filePath = yield ioUtil.tryGetExecutablePath(directory + path.sep + tool, extensions); + if (filePath) { + return filePath; + } + } + return ''; + } + catch (err) { + throw new Error(`which failed with message ${err.message}`); + } + }); +} +exports.which = which; +function readCopyOptions(options) { + const force = options.force == null ? true : options.force; + const recursive = Boolean(options.recursive); + return { force, recursive }; +} +function cpDirRecursive(sourceDir, destDir, currentDepth, force) { + return __awaiter(this, void 0, void 0, function* () { + // Ensure there is not a run away recursive copy + if (currentDepth >= 255) + return; + currentDepth++; + yield mkdirP(destDir); + const files = yield ioUtil.readdir(sourceDir); + for (const fileName of files) { + const srcFile = `${sourceDir}/${fileName}`; + const destFile = `${destDir}/${fileName}`; + const srcFileStat = yield ioUtil.lstat(srcFile); + if (srcFileStat.isDirectory()) { + // Recurse + yield cpDirRecursive(srcFile, destFile, currentDepth, force); + } + else { + yield copyFile(srcFile, destFile, force); + } + } + // Change the mode for the newly created directory + yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); + }); +} +// Buffered file copy +function copyFile(srcFile, destFile, force) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { + // unlink/re-link it + try { + yield ioUtil.lstat(destFile); + yield ioUtil.unlink(destFile); + } + catch (e) { + // Try to override file permission + if (e.code === 'EPERM') { + yield ioUtil.chmod(destFile, '0666'); + yield ioUtil.unlink(destFile); + } + // other errors = it doesn't exist, no work to do + } + // Copy over symlink + const symlinkFull = yield ioUtil.readlink(srcFile); + yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); + } + else if (!(yield ioUtil.exists(destFile)) || force) { + yield ioUtil.copyFile(srcFile, destFile); + } + }); +} +//# sourceMappingURL=io.js.map + +/***/ }), + /***/ 965: /***/ (function() { diff --git a/index.js b/index.js index 7687349..1b22680 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ const core = require('@actions/core'); const exec = require('@actions/exec'); +const io = require('@actions/io'); const fetch = require('node-fetch'); const fs = require('fs'); const semver = require('semver'); @@ -22,6 +23,12 @@ async function run() { const configuration = core.getInput('configuration') || 'Debug'; const agentVersion = core.getInput('agentVersion'); + //If project uses testplan force use of code coverage + let file_list = recFindByExt('.','xctestplan'); + for(let testPlanFile of file_list ){ + await deleteLinesContaining(testPlanFile, 'codeCoverage') + } + //Read project const workspace = await getWorkspace(); let xcodeproj = await getXCodeProj(); @@ -47,7 +54,6 @@ async function run() { console.log(`Scheme selected: ${scheme}`); //copy configfile - const configfileName = 'scopeConfig.xcconfig'; const configFilePath = scopeDir + '/' + configfileName; @@ -57,8 +63,6 @@ async function run() { } createXCConfigFile(configFilePath); - //enableCodeCoverage in xcodebuild doesn't work with test plans, configure them before - configureTestPlansForCoverage(projectParameter, scheme); //download scope await downloadLatestScope(agentVersion); @@ -249,6 +253,7 @@ async function downloadLatestScope(agentVersion) { }); const scopeURL = versions[agentVersion] || versions[currentVersion]; const scopePath = scopeDir + '/scopeAgent.zip'; + console.log(`Scope agent downloading: ${scopeURL}`); await downloadFile(scopeURL, scopePath); const extractCommand = 'ditto -x -k ' + scopePath + ' ' + scopeDir + '/scopeAgent'; @@ -334,32 +339,6 @@ async function insertEnvVariable( name, value, file, target) { } } -async function configureTestPlansForCoverage( projectParameter, scheme ) { - //Check if project is configured with test plans - let showTestPlansCommand = 'xcodebuild -showTestPlans -json ' + projectParameter + ' -scheme ' + scheme; - let auxOutput = ''; - const options = {}; - options.listeners = { - stdout: (data) => { - auxOutput += data.toString(); - } - }; - await exec.exec(showTestPlansCommand, null, options); - const showTestPlans = JSON.parse(auxOutput); - if( showTestPlans.testPlans === null ) { - return; - } - - //If uses testplan configure to use code coverage - let file_list = recFindByExt('.','xctestplan'); - for(let testPlanFile of file_list ){ - let rawdata = fs.readFileSync(testPlanFile); - let testPlan = JSON.parse(rawdata); - testPlan.defaultOptions.codeCoverage = true; - fs.writeFileSync(testPlanFile, JSON.stringify(testPlan)); - } -} - function recFindByExt(base,ext,files,result) { files = files || fs.readdirSync(base); @@ -384,4 +363,34 @@ function recFindByExt(base,ext,files,result) return result } +async function deleteLinesContaining( file, match ) { + let newName = file + '_old'; + await io.mv(file, newName ); + fs.readFile(newName, {encoding: 'utf-8'}, function(err, data) { + if (err) throw error; + + let dataArray = data.split('\n'); // convert file data in an array + const searchKeyword = match; // we are looking for a line, contains, key word 'user1' in the file + let lastIndex = -1; // let say, we have not found the keyword + + for (let index=0; index { + if (err) throw err; + console.log ('Successfully updated the file data'); + }); + + }); +} run(); diff --git a/package-lock.json b/package-lock.json index 82b68a7..0f10263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,11 @@ "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz", "integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" }, + "@actions/io": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz", + "integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", diff --git a/package.json b/package.json index d7e2794..5d4f2cf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@actions/core": "^1.2.0", "@actions/exec": "^1.0.1", + "@actions/io": "^1.0.1", "node-fetch": "^2.6.0", "semver": "^6.3.0", "shelljs": "^0.8.3"