diff --git a/src/cli.ts b/src/cli.ts index d021a9984..6673b3b87 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,7 +19,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import { Builders, Enums, Factories, Serialize, Spec, Validation } from '@cyclonedx/cyclonedx-library' import { Argument, Command, Option } from 'commander' -import { existsSync, openSync } from 'fs' +import { existsSync, mkdirSync, openSync } from 'fs' import { dirname, resolve } from 'path' import { loadJsonFile, writeAllSync } from './_helpers' @@ -315,7 +315,11 @@ export async function run (process: NodeJS.Process): Promise { } } } - + const directory = dirname(options.outputFile) + if (!existsSync(directory)) { + myConsole.info('INFO | creating directory', directory) + mkdirSync(directory, { recursive: true }) + } myConsole.log('LOG | writing BOM to', options.outputFile) const written = await writeAllSync( options.outputFile === OutputStdOut diff --git a/tests/_helper/index.js b/tests/_helper/index.js index cd01ed642..c3ea4fd6e 100644 --- a/tests/_helper/index.js +++ b/tests/_helper/index.js @@ -155,8 +155,17 @@ function getNpmVersion () { return v } +/** + * @param {string} s + * @return {string} + */ +function regexEscape (s) { + return s.replace(/[\^$(){}[\]+*?.|\\-]/g, '\\$&') +} + module.exports = { hashFile, makeReproducible, - getNpmVersion + getNpmVersion, + regexEscape } diff --git a/tests/integration/cli.from-setups.test.js b/tests/integration/cli.from-setups.test.js index 6c3d7823b..a05c4c6e6 100644 --- a/tests/integration/cli.from-setups.test.js +++ b/tests/integration/cli.from-setups.test.js @@ -18,12 +18,12 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ const { spawnSync } = require('child_process') -const { join } = require('path') -const { writeFileSync, readFileSync, mkdirSync } = require('fs') +const { dirname, join } = require('path') +const { writeFileSync, readFileSync, existsSync } = require('fs') const { describe, expect, test } = require('@jest/globals') -const { makeReproducible, getNpmVersion } = require('../_helper') +const { makeReproducible, getNpmVersion, regexEscape } = require('../_helper') const { UPDATE_SNAPSHOTS, mkTemp, cliWrapper, latestCdxSpecVersion, dummyProjectsRoot, dummyResultsRoot, projectDemoRootPath, demoResultsRoot } = require('./') describe('integration.cli.from-setups', () => { @@ -37,9 +37,6 @@ describe('integration.cli.from-setups', () => { const formats = ['json', 'xml'] describe('dummy_projects', () => { - const tmpRootRun = join(tmpRoot, 'with-prepared') - mkdirSync(tmpRootRun) - const useCases = [ { subject: 'bare', @@ -60,7 +57,9 @@ describe('integration.cli.from-setups', () => { function runTest (subject, project, format, additionalCliArgs = []) { const expectedOutSnap = join(dummyResultsRoot, subject, `${project}.snap.${format}`) - const outFile = join(tmpRoot, `${subject}_${project}.${format}`) + const outFile = join(tmpRoot, subject, `${project}.${format}`) + const outDirExisted = existsSync(dirname(outFile)) + // no need to create that outFile dir first - the tool is expected to do that for us const res = spawnSync( process.execPath, ['--', cliWrapper, @@ -69,10 +68,11 @@ describe('integration.cli.from-setups', () => { '--output-format', format, '--output-reproducible', '--output-file', outFile, - '--validate' + '--validate', + '-vvv' ], { cwd: join(dummyProjectsRoot, project), - stdio: ['ignore', 'inherit', 'pipe'], + stdio: ['ignore', 'ignore', 'pipe'], encoding: 'utf8' } ) @@ -84,6 +84,11 @@ describe('integration.cli.from-setups', () => { process.stderr.write('\n') throw err } + + const expectStdErr = expect(res.stderr); + (outDirExisted ? expectStdErr.not : expectStdErr).toContain(`creating directory ${dirname(outFile)}`) + expectStdErr.toMatch(new RegExp(`wrote \\d+ bytes to ${regexEscape(outFile)}`)) + const actualOutput = makeReproducible(format, readFileSync(outFile, 'utf8')) if (UPDATE_SNAPSHOTS) { @@ -97,8 +102,6 @@ describe('integration.cli.from-setups', () => { } describe.each(useCases)('subject: $subject', (ud) => { - mkdirSync(join(tmpRootRun, ud.subject)) - describe.each(ud.dummyProject)('dummyProject: %s', (dummyProject) => { describe.each(formats)('format: %s', (format) => { (skipAllTests