diff --git a/src/modules/system/index.ts b/src/modules/system/index.ts index 46a12b19156..6d2acca083f 100644 --- a/src/modules/system/index.ts +++ b/src/modules/system/index.ts @@ -22,6 +22,16 @@ const commonInterfaceSchemas = { pci: 'p', } as const; +const CRON_DAY_OF_WEEK = [ + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', +] as const; + /** * Generates fake data for many computer systems properties. */ @@ -264,4 +274,67 @@ export class System { return `${prefix}${interfaceType}${commonInterfaceSchemas[interfaceSchema]}${suffix}`; } + + /** + * Returns a random cron expression. + * + * @param options The optional options to use. + * @param options.includeYear Whether to include a year in the generated expression. Defaults to `false`. + * @param options.includeNonStandard Whether to include a @yearly, @monthly, @daily, etc text labels in the generated expression. Defaults to `false`. + * + * @example + * faker.system.cron() // '45 23 * * 6' + * faker.system.cron({ includeYear: true }) // '45 23 * * 6 2067' + * faker.system.cron({ includeYear: false }) // '45 23 * * 6' + * faker.system.cron({ includeNonStandard: false }) // '45 23 * * 6' + * faker.system.cron({ includeNonStandard: true }) // '@yearly' + */ + cron( + options: { + includeYear?: boolean; + includeNonStandard?: boolean; + } = {} + ): string { + const { includeYear = false, includeNonStandard = false } = options; + + // create the arrays to hold the available values for each component of the expression + const minutes = [this.faker.datatype.number({ min: 0, max: 59 }), '*']; + const hours = [this.faker.datatype.number({ min: 0, max: 23 }), '*']; + const days = [this.faker.datatype.number({ min: 1, max: 31 }), '*', '?']; + const months = [this.faker.datatype.number({ min: 1, max: 12 }), '*']; + const daysOfWeek = [ + this.faker.datatype.number({ min: 0, max: 6 }), + this.faker.helpers.arrayElement(CRON_DAY_OF_WEEK), + '*', + '?', + ]; + const years = [this.faker.datatype.number({ min: 1970, max: 2099 }), '*']; + + const minute = this.faker.helpers.arrayElement(minutes); + const hour = this.faker.helpers.arrayElement(hours); + const day = this.faker.helpers.arrayElement(days); + const month = this.faker.helpers.arrayElement(months); + const dayOfWeek = this.faker.helpers.arrayElement(daysOfWeek); + const year = this.faker.helpers.arrayElement(years); + + // create and return the cron expression string + let standardExpression = `${minute} ${hour} ${day} ${month} ${dayOfWeek}`; + if (includeYear) { + standardExpression += ` ${year}`; + } + + const nonStandardExpressions = [ + '@annually', + '@daily', + '@hourly', + '@monthly', + '@reboot', + '@weekly', + '@yearly', + ]; + + return !includeNonStandard || this.faker.datatype.boolean() + ? standardExpression + : this.faker.helpers.arrayElement(nonStandardExpressions); + } } diff --git a/test/__snapshots__/system.spec.ts.snap b/test/__snapshots__/system.spec.ts.snap index 3cbabb8f641..ef22470d8c2 100644 --- a/test/__snapshots__/system.spec.ts.snap +++ b/test/__snapshots__/system.spec.ts.snap @@ -8,6 +8,16 @@ exports[`system > 42 > commonFileName > with extension 1`] = `"lavender_shoes.ex exports[`system > 42 > commonFileType 1`] = `"audio"`; +exports[`system > 42 > cron > noArgs 1`] = `"* 19 * 3 5"`; + +exports[`system > 42 > cron > with includeNonStandard false 1`] = `"* 19 * 3 5"`; + +exports[`system > 42 > cron > with includeNonStandard true 1`] = `"@yearly"`; + +exports[`system > 42 > cron > with includeYear false 1`] = `"* 19 * 3 5"`; + +exports[`system > 42 > cron > with includeYear true 1`] = `"* 19 * 3 5 2047"`; + exports[`system > 42 > directoryPath 1`] = `"/opt/bin"`; exports[`system > 42 > fileExt > noArgs 1`] = `"lrm"`; @@ -76,6 +86,16 @@ exports[`system > 1211 > commonFileName > with extension 1`] = `"invoice_cyclocr exports[`system > 1211 > commonFileType 1`] = `"application"`; +exports[`system > 1211 > cron > noArgs 1`] = `"55 * 28 * 1"`; + +exports[`system > 1211 > cron > with includeNonStandard false 1`] = `"55 * 28 * 1"`; + +exports[`system > 1211 > cron > with includeNonStandard true 1`] = `"@hourly"`; + +exports[`system > 1211 > cron > with includeYear false 1`] = `"55 * 28 * 1"`; + +exports[`system > 1211 > cron > with includeYear true 1`] = `"55 * 28 * 1 *"`; + exports[`system > 1211 > directoryPath 1`] = `"/var/log"`; exports[`system > 1211 > fileExt > noArgs 1`] = `"dic"`; @@ -144,6 +164,16 @@ exports[`system > 1337 > commonFileName > with extension 1`] = `"nesciunt.ext"`; exports[`system > 1337 > commonFileType 1`] = `"audio"`; +exports[`system > 1337 > cron > noArgs 1`] = `"15 13 5 * *"`; + +exports[`system > 1337 > cron > with includeNonStandard false 1`] = `"15 13 5 * *"`; + +exports[`system > 1337 > cron > with includeNonStandard true 1`] = `"@yearly"`; + +exports[`system > 1337 > cron > with includeYear false 1`] = `"15 13 5 * *"`; + +exports[`system > 1337 > cron > with includeYear true 1`] = `"15 13 5 * * 2029"`; + exports[`system > 1337 > directoryPath 1`] = `"/Library"`; exports[`system > 1337 > fileExt > noArgs 1`] = `"oa3"`; @@ -210,6 +240,8 @@ exports[`system > seed: 42 > commonFileName() 1`] = `"lavender_shoes.mpe"`; exports[`system > seed: 42 > commonFileType() 1`] = `"audio"`; +exports[`system > seed: 42 > cron() 1`] = `"* 19 * 3 5"`; + exports[`system > seed: 42 > directoryPath() 1`] = `"/opt/bin"`; exports[`system > seed: 42 > fileExt() 1`] = `"lrm"`; @@ -232,6 +264,8 @@ exports[`system > seed: 1211 > commonFileName() 1`] = `"invoice_cyclocross_assau exports[`system > seed: 1211 > commonFileType() 1`] = `"application"`; +exports[`system > seed: 1211 > cron() 1`] = `"55 * 28 * 1"`; + exports[`system > seed: 1211 > directoryPath() 1`] = `"/var/log"`; exports[`system > seed: 1211 > fileExt() 1`] = `"dic"`; @@ -254,6 +288,8 @@ exports[`system > seed: 1337 > commonFileName() 1`] = `"nesciunt.mp2"`; exports[`system > seed: 1337 > commonFileType() 1`] = `"audio"`; +exports[`system > seed: 1337 > cron() 1`] = `"15 13 5 * *"`; + exports[`system > seed: 1337 > directoryPath() 1`] = `"/Library"`; exports[`system > seed: 1337 > fileExt() 1`] = `"oa3"`; diff --git a/test/system.spec.ts b/test/system.spec.ts index 05e2c53daba..fb0ef209a2e 100644 --- a/test/system.spec.ts +++ b/test/system.spec.ts @@ -10,6 +10,7 @@ const functionNames = [ 'commonFileExt', 'commonFileName', 'commonFileType', + 'cron', 'directoryPath', 'fileExt', 'fileName', @@ -65,6 +66,14 @@ describe('system', () => { } } }); + + t.describe('cron', (t) => { + t.it('noArgs') + .it('with includeYear true', { includeYear: true }) + .it('with includeYear false', { includeYear: false }) + .it('with includeNonStandard true', { includeNonStandard: true }) + .it('with includeNonStandard false', { includeNonStandard: false }); + }); }); for (const seed of seededRuns) { @@ -385,6 +394,41 @@ describe('system', () => { ).toMatch(/^enx[a-f\d]{12}$/); }); }); + + describe('cron()', () => { + const regex = + /^([1-9]|[1-5]\d|\*) ([0-9]|1\d|2[0-3]|\*) ([1-9]|[12]\d|3[01]|\*|\?) ([1-9]|1[0-2]|\*) ([0-6]|\*|\?|[A-Z]{3}) ((19[7-9]d)|20\d{2}|\*)?/; + + const regexElements = regex.toString().replace(/\//g, '').split(' '); + + it.each([ + [{}, 5], + [{ includeYear: false }, 5], + [{ includeYear: true }, 6], + ])( + 'should return cron expression with correct number of valid elements - %o, %d', + (options, count: number) => { + const cron = faker.system.cron(options).split(' '); + expect(cron).toHaveLength(count); + cron.forEach((cronElement, i) => + expect( + cronElement, + `generated cron, ${cronElement} should match regex ${regexElements[i]}` + ).toMatch(new RegExp(regexElements[i])) + ); + } + ); + + it('should return non-standard cron expressions', () => { + const validResults = ['1', '2', '5', '*', '@']; + expect( + faker.system.cron({ includeNonStandard: true })[0], + 'generated cron, string should contain non-standard cron labels' + ).toSatisfy( + (value) => !!validResults.find((result) => value === result) + ); + }); + }); } });