diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 0e53ae7b11..0b1c562522 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -39,6 +39,7 @@ jobs: - javascript,schema-javascript - golang,schema-golang - cjson,schema-cjson + - cjson-default,cjson-multi-header,cjson-multi-split - cplusplus,schema-cplusplus - flow,schema-flow - java,schema-java diff --git a/.gitignore b/.gitignore index 7178f4122b..747cce08bd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,7 @@ test/golang/schema-from-schema.json test/elm/elm-stuff/ test/elm/elm.js test/elm/QuickType.elm -test/fixtures/cjson/cJSON.* -test/fixtures/cjson/hashtable.* -test/fixtures/cjson/list.* +test/fixtures/cjson/deps/ test/fixtures/rust/target test/fixtures/java/target test/fixtures/java-lombok/target diff --git a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts index c1f3544657..c750529c98 100644 --- a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts +++ b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts @@ -57,7 +57,13 @@ import { } from "./utils.js"; export class CJSONRenderer extends ConvenienceRenderer { - private currentFilename: string | undefined; /* Current filename */ + private currentHeaderFilename: + | string + | undefined; /* Current header filename */ + + private currentSourceFilename: + | string + | undefined; /* Current source filename */ private readonly memberNameStyle: NameStyle; /* Member name style */ @@ -231,8 +237,8 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to create header file(s) - * @param proposedFilename: source filename provided from stdin + * Function called to create header and source file(s) + * @param proposedFilename: source filename provided from stdin (without extensions) */ protected emitSourceStructure(proposedFilename: string): void { /* Depending of source style option, generate a unique header or multiple header files */ @@ -244,12 +250,12 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to create a single header file with types and generators + * Function called to create a single pair of header/source files with types and generators * @param proposedFilename: source filename provided from stdin */ protected emitSingleSourceStructure(proposedFilename: string): void { - /* Create file */ - this.startFile(proposedFilename); + /* Create header file */ + this.startHeaderFile(proposedFilename); /* Create types */ this.forEachDeclaration("leading-and-interposing", (decl) => { @@ -306,6 +312,14 @@ export class CJSONRenderer extends ConvenienceRenderer { (type) => this.namedTypeToNameForTopLevel(type) === undefined, ); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(proposedFilename); + } + /* Create enum functions */ this.forEachEnum( "leading-and-interposing", @@ -333,8 +347,7 @@ export class CJSONRenderer extends ConvenienceRenderer { (type) => this.namedTypeToNameForTopLevel(type) === undefined, ); - /* Close file */ - this.finishFile(); + this.finishCurrentFile(); } /** @@ -371,12 +384,12 @@ export class CJSONRenderer extends ConvenienceRenderer { protected emitEnum(enumType: EnumType): void { /* Create file */ const enumName = this.nameForNamedType(enumType); - const filename = this.sourcelikeToString(enumName).concat(".h"); - this.includes.push(filename); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(enumName).concat(".h"); + this.includes.push(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes */ - this.emitIncludes(enumType, this.sourcelikeToString(filename)); + this.emitIncludes(enumType, this.sourcelikeToString(headerFilename)); /* Create types */ this.emitEnumTypedef(enumType); @@ -384,11 +397,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitEnumPrototypes(enumType); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(headerFilename); + } + /* Create functions */ this.emitEnumFunctions(enumType); - /* Close file */ - this.finishFile(); + this.finishCurrentFile(); } /** @@ -548,12 +568,12 @@ export class CJSONRenderer extends ConvenienceRenderer { protected emitUnion(unionType: UnionType): void { /* Create file */ const unionName = this.nameForNamedType(unionType); - const filename = this.sourcelikeToString(unionName).concat(".h"); - this.includes.push(filename); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(unionName).concat(".h"); + this.includes.push(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes */ - this.emitIncludes(unionType, this.sourcelikeToString(filename)); + this.emitIncludes(unionType, this.sourcelikeToString(headerFilename)); /* Create types */ this.emitUnionTypedef(unionType); @@ -561,11 +581,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitUnionPrototypes(unionType); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(headerFilename); + } + /* Create functions */ this.emitUnionFunctions(unionType); - /* Close file */ - this.finishFile(); + this.finishCurrentFile(); } /** @@ -1991,12 +2018,12 @@ export class CJSONRenderer extends ConvenienceRenderer { protected emitClass(classType: ClassType): void { /* Create file */ const className = this.nameForNamedType(classType); - const filename = this.sourcelikeToString(className).concat(".h"); - this.includes.push(filename); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(className).concat(".h"); + this.includes.push(headerFilename); + this.startHeaderFile(headerFilename); /* Create includes */ - this.emitIncludes(classType, this.sourcelikeToString(filename)); + this.emitIncludes(classType, this.sourcelikeToString(headerFilename)); /* Create types */ this.emitClassTypedef(classType); @@ -2004,11 +2031,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitClassPrototypes(classType); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(headerFilename); + } + /* Create functions */ this.emitClassFunctions(classType); - /* Close file */ - this.finishFile(); + this.finishCurrentFile(); } /** @@ -4598,8 +4632,8 @@ export class CJSONRenderer extends ConvenienceRenderer { includes: string[], ): void { /* Create file */ - const filename = this.sourcelikeToString(className).concat(".h"); - this.startFile(filename); + const headerFilename = this.sourcelikeToString(className).concat(".h"); + this.startHeaderFile(headerFilename); /* Create includes - This create too much includes but this is safer because of specific corner cases */ includes.forEach((name) => { @@ -4613,11 +4647,18 @@ export class CJSONRenderer extends ConvenienceRenderer { /* Create prototypes */ this.emitTopLevelPrototypes(type, className); + if (!this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + + /* Create source file */ + this.startSourceFile(headerFilename); + } + /* Create functions */ this.emitTopLevelFunctions(type, className); - /* Close file */ - this.finishFile(); + this.finishCurrentFile(); } /** @@ -5535,24 +5576,25 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to create a file + * Function called to create a header file * @param proposedFilename: source filename provided from stdin */ - protected startFile(proposedFilename: Sourcelike): void { - /* Check if previous file is closed, create a new file */ + protected startHeaderFile(proposedFilename: Sourcelike): void { + /* Check if previous header file is closed, create a new file */ assert( - this.currentFilename === undefined, - "Previous file wasn't finished", + this.currentHeaderFilename === undefined, + "Previous header file wasn't finished", ); if (proposedFilename !== undefined) { - this.currentFilename = this.sourcelikeToString(proposedFilename); + this.currentHeaderFilename = + this.sourcelikeToString(proposedFilename); } - /* Check if file has been created */ - if (this.currentFilename !== undefined) { + /* Check if header file has been created */ + if (this.currentHeaderFilename !== undefined) { /* Write header */ this.emitDescriptionBlock([ - this.currentFilename, + this.currentHeaderFilename, "This file has been autogenerated using quicktype https://github.com/quicktype/quicktype - DO NOT EDIT", "This file depends of https://github.com/DaveGamble/cJSON, https://github.com/joelguittet/c-list and https://github.com/joelguittet/c-hashtable", "To parse json data from json string use the following: struct * data = cJSON_Parse();", @@ -5567,7 +5609,7 @@ export class CJSONRenderer extends ConvenienceRenderer { this.emitLine( "#ifndef __", allUpperWordStyle( - this.currentFilename.replace( + this.currentHeaderFilename.replace( new RegExp(/[^a-zA-Z0-9]+/, "g"), "_", ), @@ -5577,7 +5619,7 @@ export class CJSONRenderer extends ConvenienceRenderer { this.emitLine( "#define __", allUpperWordStyle( - this.currentFilename.replace( + this.currentHeaderFilename.replace( new RegExp(/[^a-zA-Z0-9]+/, "g"), "_", ), @@ -5617,11 +5659,42 @@ export class CJSONRenderer extends ConvenienceRenderer { } /** - * Function called to close current file + * Function called to create a source file + * @param headerFilename: filename of the header file corresponding to this source file + */ + protected startSourceFile(headerFilename: Sourcelike): void { + /* Check if previous source file is closed, create a new file */ + assert( + this.currentSourceFilename === undefined, + "Previous source file wasn't finished", + ); + if (headerFilename !== undefined) { + this.currentSourceFilename = this.getSourceNameFromHeaderName( + this.sourcelikeToString(headerFilename), + ); + } + + /* Check if source file has been created */ + if (this.currentSourceFilename !== undefined) { + /* Write header */ + this.emitDescriptionBlock([ + this.currentSourceFilename, + "This file has been autogenerated using quicktype https://github.com/quicktype/quicktype - DO NOT EDIT", + ]); + this.ensureBlankLine(); + + /* Include corresponding header file */ + this.emitIncludeLine(this.sourcelikeToString(headerFilename)); + this.ensureBlankLine(); + } + } + + /** + * Function called to close current header file */ - protected finishFile(): void { - /* Check if file has been created */ - if (this.currentFilename !== undefined) { + protected finishHeaderFile(): void { + /* Check if header file has been created */ + if (this.currentHeaderFilename !== undefined) { /* Write C++ guard */ this.emitLine("#ifdef __cplusplus"); this.emitLine("}"); @@ -5632,7 +5705,7 @@ export class CJSONRenderer extends ConvenienceRenderer { this.emitLine( "#endif /* __", allUpperWordStyle( - this.currentFilename.replace( + this.currentHeaderFilename.replace( new RegExp(/[^a-zA-Z0-9]+/, "g"), "_", ), @@ -5641,9 +5714,37 @@ export class CJSONRenderer extends ConvenienceRenderer { ); this.ensureBlankLine(); - /* Close file */ - super.finishFile(defined(this.currentFilename)); - this.currentFilename = undefined; + /* Close header file */ + super.finishFile(defined(this.currentHeaderFilename)); + this.currentHeaderFilename = undefined; + } + } + + /** + * Function called to close current source file + */ + protected finishSourceFile(): void { + /* Check if source file has been created */ + if (this.currentSourceFilename !== undefined) { + this.ensureBlankLine(); + + /* Close source file */ + super.finishFile(defined(this.currentSourceFilename)); + this.currentSourceFilename = undefined; + } + } + + /** + * Function called to close the current file, either the header file when + * generating headers only, or the source file otherwise + */ + protected finishCurrentFile(): void { + if (this._options.headerOnly) { + /* Close header file */ + this.finishHeaderFile(); + } else { + /* Close source file */ + this.finishSourceFile(); } } @@ -5909,4 +6010,13 @@ export class CJSONRenderer extends ConvenienceRenderer { recur(false, false, 0, type); return result; } + + /** + * Get the name of the source file corresponding to a header file + * @param headerName: header filename + * @return Source filename + */ + protected getSourceNameFromHeaderName(headerName: string): string { + return headerName.replace(/\.h$/, ".c"); + } } diff --git a/packages/quicktype-core/src/language/CJSON/language.ts b/packages/quicktype-core/src/language/CJSON/language.ts index 6ac2e956a0..f2ba2e6794 100644 --- a/packages/quicktype-core/src/language/CJSON/language.ts +++ b/packages/quicktype-core/src/language/CJSON/language.ts @@ -23,6 +23,7 @@ import type { RenderContext } from "../../Renderer.js"; import { + BooleanOption, EnumOption, StringOption, getOptionValues, @@ -117,6 +118,12 @@ export const cJSONOptions = { namingStyles, "upper-underscore-case", ), + headerOnly: new BooleanOption( + "header-only", + "Generate headers only", + true, + "secondary", + ), }; /* cJSON generator target language */ diff --git a/test/fixtures.ts b/test/fixtures.ts index d8bc01ed3f..a49aef802f 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -942,7 +942,9 @@ const commentInjectionTreeSitterTargets: TreeSitterTarget[] = [ { displayName: "cjson", language: languages.CJSONLanguage, - output: "TopLevel.c", + // CJSONLanguage renders with header-only=false, so this produces + // both TopLevel.h and TopLevel.c; both are collected and parsed. + output: "TopLevel.h", wasmModule: "tree-sitter-c/tree-sitter-c.wasm", extensions: [".c", ".h"], schema: commentInjectionSchema, @@ -1546,6 +1548,9 @@ export const allFixtures: Fixture[] = [ new JSONFixture(languages.JavaLanguageWithLombok, "java-lombok"), new JSONFixture(languages.GoLanguage), new JSONFixture(languages.CJSONLanguage), + new JSONFixture(languages.CJSONDefaultLanguage, "cjson-default"), + new JSONFixture(languages.CJSONMultiHeaderLanguage, "cjson-multi-header"), + new JSONFixture(languages.CJSONMultiSplitLanguage, "cjson-multi-split"), new JSONFixture(languages.CPlusPlusLanguage), new JSONFixture(languages.PHPLanguage), new JSONFixture(languages.RustLanguage), diff --git a/test/fixtures/cjson/second.c b/test/fixtures/cjson/second.c new file mode 100644 index 0000000000..69df0916a6 --- /dev/null +++ b/test/fixtures/cjson/second.c @@ -0,0 +1,8 @@ +/* Second translation unit: including the generated header from more than + * one .c file must compile and link (no duplicate symbol definitions). */ + +#include "TopLevel.h" + +int quicktypeSecondTranslationUnit(void) { + return 0; +} diff --git a/test/languages.ts b/test/languages.ts index d1a7107354..8efcef92bf 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -482,16 +482,26 @@ export const GoLanguage: Language = { sourceFiles: ["src/language/Golang/index.ts"], }; +/* The vendored dependencies are downloaded into deps/ and included via + * -isystem so that generated cross-file includes must be quoted includes + * (resolved relative to the including file): a generated + * `#include ` fails to compile. */ +const cJSONSetupCommand = + "mkdir -p deps && curl -o deps/cJSON.c https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.c && curl -o deps/cJSON.h https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.h && curl -o deps/list.h https://raw.githubusercontent.com/joelguittet/c-list/master/include/list.h && curl -o deps/list.c https://raw.githubusercontent.com/joelguittet/c-list/master/src/list.c && curl -o deps/hashtable.h https://raw.githubusercontent.com/joelguittet/c-hashtable/master/include/hashtable.h && curl -o deps/hashtable.c https://raw.githubusercontent.com/joelguittet/c-hashtable/master/src/hashtable.c"; + +function cJSONRunCommand(sample: string): string { + return `valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1 ./quicktype "${sample}"`; +} + export const CJSONLanguage: Language = { name: "cjson", base: "test/fixtures/cjson", - setupCommand: - "curl -o cJSON.c https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.c && curl -o cJSON.h https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.15/cJSON.h && curl -o list.h https://raw.githubusercontent.com/joelguittet/c-list/master/include/list.h && curl -o list.c https://raw.githubusercontent.com/joelguittet/c-list/master/src/list.c && curl -o hashtable.h https://raw.githubusercontent.com/joelguittet/c-hashtable/master/include/hashtable.h && curl -o hashtable.c https://raw.githubusercontent.com/joelguittet/c-hashtable/master/src/hashtable.c", + setupCommand: cJSONSetupCommand, + /* second.c is a second translation unit including TopLevel.h; it verifies + * that the generated header/source split supports multi-TU builds. */ compileCommand: - "gcc -O0 -o quicktype -I. cJSON.c hashtable.c list.c main.c -lpthread", - runCommand(sample: string) { - return `valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1 ./quicktype "${sample}"`; - }, + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c main.c second.c TopLevel.c -lpthread", + runCommand: cJSONRunCommand, diffViaSchema: true, skipDiffViaSchema: [ /* Enum constants are different when generating with schema */ @@ -566,8 +576,88 @@ export const CJSONLanguage: Language = { /* Class elements with invalid type are not checked (for the current implementation, can be added later, should abord parsing and return NULL) */ ...skipsUntypedUnions, ], + rendererOptions: { "header-only": "false" }, + quickTestRendererOptions: [ + { "source-style": "single-source", "header-only": "false" }, + ], + sourceFiles: ["src/language/CJSON/index.ts"], +}; + +/* Minimal fixtures covering the remaining source-style / header-only mode + * combinations on a single complex input (enums, unions, many classes). + * They share the cjson driver directory and setup. */ + +/* Default options: single-source, header-only. Single translation unit, + * no generated TopLevel.c — the pre-existing output mode. */ +export const CJSONDefaultLanguage: Language = { + name: "cjson", + base: "test/fixtures/cjson", + setupCommand: cJSONSetupCommand, + compileCommand: + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c main.c -lpthread", + runCommand: cJSONRunCommand, + diffViaSchema: false, + skipDiffViaSchema: [], + allowMissingNull: false, + features: [], + output: "TopLevel.h", + topLevel: "TopLevel", + includeJSON: ["nbl-stats.json"], + skipMiscJSON: true, + skipSchema: [], rendererOptions: {}, - quickTestRendererOptions: [{ "source-style": "single-source" }], + quickTestRendererOptions: [], + sourceFiles: ["src/language/CJSON/index.ts"], +}; + +/* Multi-source, header-only. One header per type; still a single + * translation unit, since header-only output defines functions in the + * headers and cannot link from multiple translation units. */ +export const CJSONMultiHeaderLanguage: Language = { + name: "cjson", + base: "test/fixtures/cjson", + setupCommand: cJSONSetupCommand, + compileCommand: + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c main.c -lpthread", + runCommand: cJSONRunCommand, + diffViaSchema: false, + skipDiffViaSchema: [], + allowMissingNull: false, + features: [], + output: "TopLevel.h", + topLevel: "TopLevel", + includeJSON: ["nbl-stats.json"], + skipMiscJSON: true, + skipSchema: [], + rendererOptions: { "source-style": "multi-source" }, + quickTestRendererOptions: [], + sourceFiles: ["src/language/CJSON/index.ts"], +}; + +/* Multi-source, split header/source pairs. The wildcard picks up main.c, + * second.c and every generated .c file; linking the two translation units + * verifies the core promise of the split mode (issue #2617). */ +export const CJSONMultiSplitLanguage: Language = { + name: "cjson", + base: "test/fixtures/cjson", + setupCommand: cJSONSetupCommand, + compileCommand: + "gcc -O0 -o quicktype -isystem deps deps/cJSON.c deps/hashtable.c deps/list.c *.c -lpthread", + runCommand: cJSONRunCommand, + diffViaSchema: false, + skipDiffViaSchema: [], + allowMissingNull: false, + features: [], + output: "TopLevel.h", + topLevel: "TopLevel", + includeJSON: ["nbl-stats.json"], + skipMiscJSON: true, + skipSchema: [], + rendererOptions: { + "source-style": "multi-source", + "header-only": "false", + }, + quickTestRendererOptions: [], sourceFiles: ["src/language/CJSON/index.ts"], }; diff --git a/test/unit/cjson-split-sources.test.ts b/test/unit/cjson-split-sources.test.ts new file mode 100644 index 0000000000..b640c41e23 --- /dev/null +++ b/test/unit/cjson-split-sources.test.ts @@ -0,0 +1,124 @@ +// cJSON can split its output into header/source pairs (header-only=false) +// and can emit one file per type (source-style=multi-source). The first +// version of the split emitted a `#include ` self-include at +// the top of every generated source file (an unguarded self-include that +// recurses at compile time), referenced generated headers with angle +// brackets instead of the quoted-include convention, and made every header +// include itself in the pre-existing multi-source header-only mode. These +// tests pin down the include structure of the generated files. +import { describe, expect, test } from "vitest"; + +import { + InputData, + type RendererOptions, + jsonInputForTargetLanguage, + quicktypeMultiFile, +} from "quicktype-core"; + +async function cJSONFiles( + rendererOptions: RendererOptions, + outputFilename = "TopLevel.h", +): Promise> { + const jsonInput = jsonInputForTargetLanguage("cjson"); + await jsonInput.addSource({ + name: "TopLevel", + samples: [ + '{"child": {"n": 1}, "color": "red", "value": 1}', + '{"child": {"n": 2}, "color": "green", "value": "s"}', + ], + }); + const inputData = new InputData(); + inputData.addInput(jsonInput); + const result = await quicktypeMultiFile({ + inputData, + lang: "cjson", + outputFilename, + rendererOptions, + }); + return new Map( + Array.from(result, ([filename, serialized]) => [ + filename, + serialized.lines.join("\n"), + ]), + ); +} + +function includesIn(source: string): string[] { + return source.match(/#include [<"][^>"]+[>"]/g) ?? []; +} + +describe("cJSON multi-source header/source pairs", () => { + const rendererOptions: RendererOptions = { + "source-style": "multi-source", + "header-only": false, + }; + + test("every header gets a source file", async () => { + const files = await cJSONFiles(rendererOptions); + const names = Array.from(files.keys()); + const headers = names.filter((name) => name.endsWith(".h")); + expect(headers.length).toBeGreaterThan(2); + for (const header of headers) { + expect(names).toContain(header.replace(/\.h$/, ".c")); + } + }); + + test("no generated file includes itself", async () => { + const files = await cJSONFiles(rendererOptions); + for (const [filename, source] of files) { + expect(includesIn(source)).not.toContain(`#include "${filename}"`); + expect(includesIn(source)).not.toContain(`#include <${filename}>`); + } + }); + + test("generated files are included with quotes, not angle brackets", async () => { + const files = await cJSONFiles(rendererOptions); + for (const [, source] of files) { + for (const include of includesIn(source)) { + const match = /#include <([^>]+)>/.exec(include); + if (match === null) { + continue; + } + + // Angle brackets are reserved for system and vendored + // headers; a generated file must never appear in them. + expect(files.has(match[1])).toBe(false); + } + } + }); + + test("each source file includes its own header first", async () => { + const files = await cJSONFiles(rendererOptions); + for (const [filename, source] of files) { + if (!filename.endsWith(".c")) { + continue; + } + + const header = filename.replace(/\.c$/, ".h"); + expect(includesIn(source)[0]).toBe(`#include "${header}"`); + } + }); +}); + +describe("cJSON multi-source header-only mode", () => { + test("emits no source files and no header includes itself", async () => { + const files = await cJSONFiles({ "source-style": "multi-source" }); + expect(files.size).toBeGreaterThan(2); + for (const [filename, source] of files) { + expect(filename).toMatch(/\.h$/); + expect(includesIn(source)).not.toContain(`#include "${filename}"`); + expect(includesIn(source)).not.toContain(`#include <${filename}>`); + } + }); +}); + +describe("cJSON source filename derivation", () => { + test("only a trailing .h is swapped for .c", async () => { + // `.replace(".h", ".c")` would have produced "my.couse.h". + const files = await cJSONFiles({ "header-only": false }, "my.house.h"); + expect(Array.from(files.keys()).sort()).toEqual([ + "my.house.c", + "my.house.h", + ]); + }); +});