diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/bin/cli b/lib/node_modules/@stdlib/_tools/doctest/c/bin/cli new file mode 100755 index 000000000000..4c16a9522211 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/bin/cli @@ -0,0 +1,141 @@ +#!/usr/bin/env node + +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var resolve = require( 'path' ).resolve; +var readFileSync = require( '@stdlib/fs/read-file' ).sync; +var CLI = require( '@stdlib/cli/ctor' ); +var cdoctest = require( './../lib' ); + + +// MAIN // + +/** +* Main execution sequence. +* +* @private +* @returns {void} +*/ +function main() { + var flags; + var args; + var opts; + var cli; + + // Create a command-line interface: + cli = new CLI({ + 'pkg': require( './../package.json' ), + 'options': require( './../etc/cli_opts.json' ), + 'help': readFileSync( resolve( __dirname, '..', 'docs', 'usage.txt' ), { + 'encoding': 'utf8' + }) + }); + + // Get any provided command-line options: + flags = cli.flags(); + if ( flags.help || flags.version ) { + return; + } + + // Get any provided command-line arguments: + args = cli.args(); + + if ( args.length === 0 ) { + return cli.error( new Error( 'insufficient arguments. Must provide one or more source file paths.' ) ); + } + + // Set up options: + opts = { + 'files': args + }; + if ( flags.compiler ) { + opts.compiler = flags.compiler; + } + if ( flags.include ) { + if ( typeof flags.include === 'string' ) { + opts.include = [ flags.include ]; + } else { + opts.include = flags.include; + } + } + if ( flags.libpath ) { + if ( typeof flags.libpath === 'string' ) { + opts.libpath = [ flags.libpath ]; + } else { + opts.libpath = flags.libpath; + } + } + if ( flags.library ) { + if ( typeof flags.library === 'string' ) { + opts.libraries = [ flags.library ]; + } else { + opts.libraries = flags.library; + } + } + if ( flags.source ) { + if ( typeof flags.source === 'string' ) { + opts.sourceFiles = [ flags.source ]; + } else { + opts.sourceFiles = flags.source; + } + } + if ( flags.timeout ) { + opts.timeout = parseInt( flags.timeout, 10 ); + } + if ( flags.quiet ) { + opts.verbose = false; + } + + cdoctest( opts, done ); + + /** + * Callback invoked upon completion. + * + * @private + * @param {(Error|null)} error - error object + * @param {Object} results - test results + * @returns {void} + */ + function done( error, results ) { + if ( error ) { + return cli.error( error ); + } + console.log( '\n=== C Doctest Results ===' ); // eslint-disable-line no-console + console.log( 'Total: %d', results.total ); // eslint-disable-line no-console + console.log( 'Pass: %d', results.pass ); // eslint-disable-line no-console + console.log( 'Fail: %d', results.fail ); // eslint-disable-line no-console + + if ( results.errors.length > 0 ) { + console.log( '\nErrors:' ); // eslint-disable-line no-console + var i; + for ( i = 0; i < results.errors.length; i++ ) { + console.log( ' - %s (line %d): %s', results.errors[ i ].file, results.errors[ i ].line || 0, results.errors[ i ].error ); // eslint-disable-line no-console + } + } + if ( results.fail > 0 ) { + process.exitCode = 1; + } + } +} + +main(); diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/docs/usage.txt b/lib/node_modules/@stdlib/_tools/doctest/c/docs/usage.txt new file mode 100644 index 000000000000..6e78aaa3e9c7 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/docs/usage.txt @@ -0,0 +1,2 @@ + +Usage: c-doctest [options] [ ...] diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/etc/cli_opts.json b/lib/node_modules/@stdlib/_tools/doctest/c/etc/cli_opts.json new file mode 100644 index 000000000000..9e1d45c98564 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/etc/cli_opts.json @@ -0,0 +1,40 @@ +{ + "string": [ + "compiler", + "include", + "libpath", + "library", + "source" + ], + "boolean": [ + "help", + "version", + "quiet" + ], + "alias": { + "help": [ + "h" + ], + "version": [ + "V" + ], + "compiler": [ + "c" + ], + "include": [ + "I" + ], + "libpath": [ + "L" + ], + "library": [ + "l" + ], + "source": [ + "s" + ], + "quiet": [ + "q" + ] + } +} diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/defaults.json b/lib/node_modules/@stdlib/_tools/doctest/c/lib/defaults.json new file mode 100644 index 000000000000..ae58c09f8337 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/defaults.json @@ -0,0 +1,17 @@ +{ + "compiler": "gcc", + "flags": [ + "-std=c99", + "-O2", + "-Wall" + ], + "libraries": [ + "-lm" + ], + "include": [], + "libpath": [], + "sourceFiles": [], + "verbose": true, + "timeout": 120000, + "maxBuffer": 10485760 +} diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/extract_examples.js b/lib/node_modules/@stdlib/_tools/doctest/c/lib/extract_examples.js new file mode 100644 index 000000000000..2edb2bfb7731 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/extract_examples.js @@ -0,0 +1,220 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var logger = require( 'debug' ); +var trim = require( '@stdlib/string/trim' ); + + +// VARIABLES // + +var debug = logger( 'c-doctest:extract' ); + +// Regular expression to match the start of a JSDoc-style comment: +var RE_COMMENT_START = /\/\*\*/; + +// Regular expression to match the end of a JSDoc-style comment: +var RE_COMMENT_END = /\*\//; + +// Regular expression to match @example tag: +var RE_EXAMPLE_TAG = /^\s*\*\s*@example\s*$/; + +// Regular expression to match a JSDoc tag (any tag other than @example): +var RE_OTHER_TAG = /^\s*\*\s*@(?!example)\w+/; + +// Regular expression to match a comment line content (strip leading * ): +var RE_COMMENT_LINE = /^\s*\*\s?(.*)$/; + +// Regular expression to match a "// returns" annotation: +var RE_RETURNS = /\/\/\s*returns\s+(.*)/; + +// Regular expression to detect approximate returns (prefixed with ~): +var RE_APPROX = /^~(.+)/; + + +// MAIN // + +/** +* Extracts `@example` blocks from C source file content. +* +* @param {string} content - file content +* @param {string} fpath - file path (for debug logging) +* @returns {Array} array of example objects +* +* @example +* var content = '/**\n* @example\n* double y = foo( 1.0 );\n* // returns 2.0\n*\/'; +* var examples = extractExamples( content, 'test.c' ); +* // returns [ { 'code': 'double y = foo( 1.0 );\n// returns 2.0', 'returns': [ { 'expected': '2.0', 'approximate': false } ], 'line': 2 } ] +*/ +function extractExamples( content, fpath ) { + var examples; + var lines; + var i; + + examples = []; + lines = content.split( '\n' ); + + i = 0; + while ( i < lines.length ) { + // Look for start of a JSDoc comment: + if ( RE_COMMENT_START.test( lines[ i ] ) ) { + i = parseComment( lines, i, examples, fpath ); + } else { + i += 1; + } + } + return examples; +} + +/** +* Parses a JSDoc comment block for `@example` tags. +* +* @private +* @param {StringArray} lines - file lines +* @param {number} startIdx - index of comment start +* @param {Array} examples - accumulator for example objects +* @param {string} fpath - file path (for debug logging) +* @returns {number} index after the comment block +*/ +function parseComment( lines, startIdx, examples, fpath ) { + var inExample; + var exLines; + var match; + var line; + var i; + + inExample = false; + exLines = []; + + for ( i = startIdx + 1; i < lines.length; i++ ) { + line = lines[ i ]; + + // Check for end of comment block: + if ( RE_COMMENT_END.test( line ) ) { + if ( inExample && exLines.length > 0 ) { + pushExample( examples, exLines, fpath ); + } + return i + 1; + } + + // Check for @example tag: + if ( RE_EXAMPLE_TAG.test( line ) ) { + // If we were already in an example, save the previous one: + if ( inExample && exLines.length > 0 ) { + pushExample( examples, exLines, fpath ); + } + inExample = true; + exLines = []; + exLines.startLine = i + 1; // 1-indexed line number + continue; + } + + // Check for another JSDoc tag (ends the current @example block): + if ( RE_OTHER_TAG.test( line ) ) { + if ( inExample && exLines.length > 0 ) { + pushExample( examples, exLines, fpath ); + } + inExample = false; + exLines = []; + continue; + } + + // If inside an @example block, collect lines: + if ( inExample ) { + match = RE_COMMENT_LINE.exec( line ); + if ( match ) { + exLines.push( match[ 1 ] ); + } + } + } + + // If file ended without closing comment (shouldn't happen normally): + if ( inExample && exLines.length > 0 ) { + pushExample( examples, exLines, fpath ); + } + return i; +} + +/** +* Creates an example object from collected lines and pushes to accumulator. +* +* @private +* @param {Array} examples - accumulator +* @param {Array} exLines - collected example lines (with `startLine` property) +* @param {string} fpath - file path +*/ +function pushExample( examples, exLines, fpath ) { + var assertions; + var approx; + var match; + var code; + var val; + var i; + + assertions = []; + + // Parse return annotations: + for ( i = 0; i < exLines.length; i++ ) { + match = RE_RETURNS.exec( exLines[ i ] ); + if ( match ) { + val = trim( match[ 1 ] ); + approx = RE_APPROX.exec( val ); + if ( approx ) { + assertions.push({ + 'expected': trim( approx[ 1 ] ), + 'approximate': true, + 'lineOffset': i + }); + } else { + assertions.push({ + 'expected': val, + 'approximate': false, + 'lineOffset': i + }); + } + } + } + + // Filter out blank-only examples: + code = trim( exLines.join( '\n' ) ); + if ( code.length === 0 ) { + debug( 'Skipping empty @example block at line %d in %s.', exLines.startLine, fpath ); + return; + } + // Filter out illustrative examples containing ellipsis placeholders (e.g., `foo( ... )`). + // These are architectural/documentation examples that show usage patterns, not runnable code: + if ( /\.\.\./g.test( code ) ) { + debug( 'Skipping illustrative @example block (contains ellipsis) at line %d in %s.', exLines.startLine, fpath ); + return; + } + debug( 'Found @example block at line %d in %s with %d assertion(s).', exLines.startLine, fpath, assertions.length ); + + examples.push({ + 'code': code, + 'assertions': assertions, + 'line': exLines.startLine + }); +} + + +// EXPORTS // + +module.exports = extractExamples; diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/generate_test_program.js b/lib/node_modules/@stdlib/_tools/doctest/c/lib/generate_test_program.js new file mode 100644 index 000000000000..22dce6d636df --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/generate_test_program.js @@ -0,0 +1,359 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +'use strict'; + +// MODULES // + +var logger = require( 'debug' ); + + +// VARIABLES // + +var debug = logger( 'c-doctest:generate' ); + +// Regular expression to match #include directives: +var RE_INCLUDE = /^\s*#include\s+[<"]([^>"]+)[>"]/; + +// Regular expression to match "// returns" annotations in the example code: +var RE_RETURNS = /\/\/\s*returns\s+(.*)/; + +// Regular expression to match a variable assignment line like: +// double y = func( args ); +// float y = func( args ); +// int n = func( args ); +// double complex z = func( args ); +var RE_ASSIGNMENT = /^\s*(double\s+complex|float\s+complex|double|float|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|bool|size_t|unsigned|signed|long|short|char)\s+(\w+)\s*=\s*(.+?)\s*;\s*$/; + +// Regular expression to detect the start of a function definition block. +// Matches lines like: +// static int32_t fcn( void ) { +// int my_func( int x ) { +// static double complex scale( double complex x ) { +var RE_FUNC_DEF = /^\s*(?:static\s+)?(?:(?:unsigned|signed|long|short)\s+)?(?:double\s+complex|float\s+complex|double|float|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|bool|size_t|char|void)\s+\w+\s*\([^)]*\)\s*\{/; + +// Regular expression to extract a function name from a function definition line: +var RE_FUNC_NAME = /(?:static\s+)?(?:(?:unsigned|signed|long|short)\s+)?(?:double\s+complex|float\s+complex|double|float|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|bool|size_t|char|void)\s+(\w+)\s*\(/; + +// Tag prefix for doctest result output lines: +var RESULT_TAG = '__DOCTEST_RESULT__:'; + + +// MAIN // + +/** + * Generates a C test program from an extracted example. + * + * The key design is a **single-pass interleaved** approach: as we walk the raw + * example lines, each code statement is emitted to `codeLines` and whenever a + * `// returns` annotation is encountered we insert the corresponding `printf` + * inline right there . This ensures that for + * examples with multiple `// returns`, each printf captures the variable's + * value at that point in the program, not just the final value. + * + * @param {Object} example - example object + * @param {string} example.code - example code + * @param {Array} example.assertions - array of assertion objects + * @param {number} example.line - line number in source file + * @param {string} fpath - source file path (for extracting includes) + * @param {string} [sourceContent] - original source file content (for extracting #include directives) + * @param {Object} [options] - options + * @param {boolean} [options.appendMode=false] - if true, generate only the suffix to append to the source file (for static function support) + * @returns {string} complete C program source + */ +function generateTestProgram( example, fpath, sourceContent, options ) { + var appendMode = ( options && options.appendMode ) || false; + var extraIncludes = ( options && options.extraIncludes ) || []; + var functionDefs; + var annotations; + var codeLines; + var includes; + var program; + var match; + var vars; + var line; + var i; + + debug( 'Generating test program for example at line %d from %s.', example.line, fpath ); + + includes = []; + codeLines = []; + functionDefs = []; + annotations = []; + vars = {}; + + // Add any extra includes provided by the caller: + for ( i = 0; i < extraIncludes.length; i++ ) { + includes.push( extraIncludes[ i ] ); + } + + // Always include stdio for printf and math for fabs: + includes.push( '#include ' ); + includes.push( '#include ' ); + includes.push( '#include ' ); + + // In append mode, the source file's #includes are already present (we are + // appending to the source), so we only collect #includes from the @example + // block itself (handled below in the main loop). + // In normal mode, extract #includes from the source file: + if ( !appendMode && sourceContent ) { + var srcLines = sourceContent.split( '\n' ); + for ( i = 0; i < srcLines.length; i++ ) { + if ( RE_INCLUDE.test( srcLines[ i ] ) ) { + includes.push( srcLines[ i ].trim() ); + } + } + } + + // --- Single-pass interleaved code + printf generation --- + // + // Track the most-recently-seen assigned variable so that when we hit a + // `// returns` line we can immediately emit a printf for it. + // + var rawLines = example.code.split( '\n' ); + var inFuncDef = false; + var braceDepth = 0; + var funcBlock = []; + var lastVarName = null; // most-recently-seen assigned variable name + var lastVarType = null; // C type of that variable (for bool-aware printf) + var assignMatch; + var reAssign; + + for ( i = 0; i < rawLines.length; i++ ) { + line = rawLines[ i ]; + + if ( inFuncDef ) { + funcBlock.push( '#line ' + ( example.line + 1 + i ) + ' "' + fpath + '"\n' + line ); + for ( var ci = 0; ci < line.length; ci++ ) { + if ( line[ ci ] === '{' ) { + braceDepth += 1; + } else if ( line[ ci ] === '}' ) { + braceDepth -= 1; + } + } + if ( braceDepth === 0 ) { + // End of function definition block: + functionDefs.push( funcBlock.join( '\n' ) ); + funcBlock = []; + inFuncDef = false; + } + continue; + } + + // ── `// returns` annotation: emit printf inline right now ── + match = RE_RETURNS.exec( line ); + if ( match ) { + var rawVal = match[ 1 ].trim(); + var isApprox = /^~/.test( rawVal ); + // Strip leading ~ for the stored annotation value: + var annotVal = isApprox ? rawVal.slice( 1 ).trim() : rawVal; + annotVal = annotVal.replace( /f$/, '' ); + var annotationStr = isApprox ? '~' + annotVal : annotVal; + annotations.push({ + 'value': annotationStr, + 'line': example.line + 1 + i + }); + + if ( lastVarName ) { + codeLines.push( '#line ' + ( example.line + 1 + i ) + ' "' + fpath + '"\n' + buildPrintf( lastVarName, lastVarType ) ); + } else { + debug( 'Could not find variable for `// returns` annotation at line %d of example at line %d in %s.', i, example.line, fpath ); + } + continue; + } + + // ── Blank lines: skip ── + if ( line.trim().length === 0 ) { + continue; + } + + // ── #include directives in the example code itself ── + if ( RE_INCLUDE.test( line ) ) { + includes.push( line.trim() ); + continue; + } + + // ── Start of a function definition block — hoist above main() ── + if ( RE_FUNC_DEF.test( line ) ) { + inFuncDef = true; + braceDepth = 0; + // Prepend #line to the first line of the function definition: + funcBlock = [ '#line ' + ( example.line + 1 + i ) + ' "' + fpath + '"\n' + line ]; + // Count opening braces on this first line: + for ( var cj = 0; cj < line.length; cj++ ) { + if ( line[ cj ] === '{' ) { + braceDepth += 1; + } else if ( line[ cj ] === '}' ) { + braceDepth -= 1; + } + } + if ( braceDepth === 0 ) { + // Single-line function def (rare but possible): + functionDefs.push( funcBlock.join( '\n' ) ); + funcBlock = []; + inFuncDef = false; + } + continue; + } + + // ── Regular code line — emit to main() ── + // Prepend #line for each code line: + codeLines.push( '#line ' + ( example.line + 1 + i ) + ' "' + fpath + '"\n\t' + line.trim() ); + + // Track the most-recently declared variable (declaration with type prefix): + assignMatch = RE_ASSIGNMENT.exec( line ); + if ( assignMatch ) { + lastVarName = assignMatch[ 2 ]; + lastVarType = assignMatch[ 1 ].trim(); + vars[ lastVarName ] = { 'type': lastVarType, 'name': lastVarName }; + } else { + // Also handle re-assignments without type prefix: `varName = expr;` + reAssign = /^\s*(\w+)\s*=\s*.+;\s*$/.exec( line ); + if ( reAssign ) { + // Update lastVarName but preserve lastVarType from the original + // declaration so bool-aware printf still works after reassignment: + lastVarName = reAssign[ 1 ]; + if ( vars[ lastVarName ] ) { + lastVarType = vars[ lastVarName ].type; + } + } + } + } + + // In append mode, apply prefix guard: if a hoisted function name already + // exists in the source file, prefix it with `__doctest_` to avoid + // redefinition errors, and update all references in codeLines: + if ( appendMode && sourceContent && functionDefs.length > 0 ) { + for ( var fi = 0; fi < functionDefs.length; fi++ ) { + var fnMatch = RE_FUNC_NAME.exec( functionDefs[ fi ] ); + if ( fnMatch ) { + var fnName = fnMatch[ 1 ]; + // Check if this function name appears in the source content + // as a function definition (not just in comments): + var fnCheckRe = new RegExp( '\\b' + fnName + '\\s*\\(', 'g' ); + if ( fnCheckRe.test( sourceContent ) ) { + var newName = '__doctest_' + fnName; + debug( 'Hoisted function "%s" conflicts with source; renaming to "%s".', fnName, newName ); + var renameRe = new RegExp( '\\b' + fnName + '\\b', 'g' ); + functionDefs[ fi ] = functionDefs[ fi ].replace( renameRe, newName ); + // Also rename in code lines: + for ( var cli = 0; cli < codeLines.length; cli++ ) { + codeLines[ cli ] = codeLines[ cli ].replace( renameRe, newName ); + } + } + } + } + } + + // Build the complete program string: + program = ''; + + // In append mode, always include stdio/math/stdlib (system headers have + // include guards, so double inclusion is safe). Then add any extra includes + // from the @example block that aren't already in the source. + // NOTE: We cannot use naive indexOf to check whether the source file has an + // include, because the string `#include ` may appear inside JSDoc + // comments (e.g. `* #include `) without being a real directive. + var uniqueIncludes = deduplicateIncludes( includes ); + if ( appendMode ) { + // Always emit the three standard includes: + program += '#include \n'; + program += '#include \n'; + program += '#include \n'; + + // Emit any extra includes from the @example block: + for ( i = 0; i < uniqueIncludes.length; i++ ) { + // Skip the three already emitted: + if (uniqueIncludes[ i ] === '#include ' || + uniqueIncludes[ i ] === '#include ' || + uniqueIncludes[ i ] === '#include ') { + continue; + } + program += uniqueIncludes[ i ] + '\n'; + } + program += '\n'; + } else { + program += uniqueIncludes.join( '\n' ) + '\n\n'; + } + + // Emit any hoisted function definitions before main(): + if ( functionDefs.length > 0 ) { + program += functionDefs.join( '\n\n' ) + '\n\n'; + } + + program += 'int main( void ) {\n'; + + for ( i = 0; i < codeLines.length; i++ ) { + program += codeLines[ i ] + '\n'; + } + program += '\treturn EXIT_SUCCESS;\n'; + program += '}\n'; + + debug( 'Generated test program:\n%s', program ); + + return { + 'program': program, + 'annotations': annotations + }; +} + +/** + * Builds a `printf` statement for the given variable. + * + * + * @private + * @param {string} varName - variable name + * @param {string} varType - C type of the variable (e.g. "double", "bool") + * @returns {string} printf statement (with leading tab, no trailing newline) + */ +function buildPrintf( varName, varType ) { + if ( varType === 'bool' ) { + // Print the string "true" or "false" so it matches annotations like + // `// returns true` and `// returns false`. + return '\tprintf( "' + RESULT_TAG + '%s\\n", (' + varName + ') ? "true" : "false" );'; + } + // Default: cast to double and print with full precision. + return '\tprintf( "' + RESULT_TAG + '%.17g\\n", (double)' + varName + ' );'; +} + +/** + * Deduplicates include directives. + * + * @private + * @param {StringArray} includes - array of include directives + * @returns {StringArray} deduplicated includes + */ +function deduplicateIncludes( includes ) { + var seen = {}; + var result = []; + var i; + + for ( i = 0; i < includes.length; i++ ) { + if ( !seen[ includes[ i ] ] ) { + seen[ includes[ i ] ] = true; + result.push( includes[ i ] ); + } + } + return result; +} + + +// EXPORTS // + +module.exports = generateTestProgram; diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/index.js b/lib/node_modules/@stdlib/_tools/doctest/c/lib/index.js new file mode 100644 index 000000000000..06ea0f52c073 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/index.js @@ -0,0 +1,50 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +/** +* C documentation test runner. +* +* @module @stdlib/_tools/doctest/c +* +* @example +* var cdoctest = require( '@stdlib/_tools/doctest/c' ); +* +* var opts = { +* 'files': [ '/path/to/file.c' ] +* }; +* +* cdoctest( opts, done ); +* +* function done( error, results ) { +* if ( error ) { +* throw error; +* } +* console.log( results ); +* } +*/ + +// MODULES // + +var main = require( './main.js' ); + + +// EXPORTS // + +module.exports = main; diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/main.js b/lib/node_modules/@stdlib/_tools/doctest/c/lib/main.js new file mode 100644 index 000000000000..3a65fd91c88a --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/main.js @@ -0,0 +1,680 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var resolve = require( 'path' ).resolve; +var join = require( 'path' ).join; +var dirname = require( 'path' ).dirname; +var readFileSync = require( 'fs' ).readFileSync; +var writeFileSync = require( 'fs' ).writeFileSync; +var unlinkSync = require( 'fs' ).unlinkSync; +var mkdirSync = require( 'fs' ).mkdirSync; +var rmdirSync = require( 'fs' ).rmdirSync; +var existsSync = require( 'fs' ).existsSync; +var readdirSync = require( 'fs' ).readdirSync; +var statSync = require( 'fs' ).statSync; +var exec = require( 'child_process' ).exec; +var logger = require( 'debug' ); +var now = require( '@stdlib/time/now' ); +var format = require( '@stdlib/string/format' ); +var dirname = require( '@stdlib/utils/dirname' ); +var extractExamples = require( './extract_examples.js' ); +var generateTestProgram = require( './generate_test_program.js' ); +var resolveIncludes = require( './resolve_includes.js' ); +var validate = require( './validate.js' ); +var DEFAULTS = require( './defaults.json' ); +var compareValues = require( '@stdlib/_tools/doctest/compare-values' ); +var createAnnotationValue = require( '@stdlib/_tools/doctest/create-annotation-value' ); + + +// VARIABLES // + +var debug = logger( 'c-doctest' ); + +// Tag prefix for doctest result output lines: +var RESULT_TAG = '__DOCTEST_RESULT__:'; + +// Regular expression to detect `static` function definitions with JSDoc @example blocks. +// We look for a JSDoc comment containing @example followed by a `static` function: +var RE_STATIC_FUNC = /^\s*static\s+(?:(?:unsigned|signed|long|short)\s+)?(?:double\s+complex|float\s+complex|double|float|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|bool|size_t|char|void)\s+\w+\s*\(/; + + +// MAIN // + +/** +* Runs C documentation tests extracted from source files. +* +* @param {Options} options - options +* @param {StringArray} options.files - list of source file paths to test +* @param {string} [options.compiler='gcc'] - C compiler to use +* @param {Array} [options.flags] - compiler flags +* @param {Array} [options.libraries] - libraries to link +* @param {Array} [options.include] - include directories +* @param {Array} [options.libpath] - library paths +* @param {Array} [options.sourceFiles] - additional source files +* @param {boolean} [options.verbose=true] - verbose output +* @param {NonNegativeInteger} [options.timeout=60000] - maximum number of milliseconds allotted for compilation and execution +* @param {PositiveInteger} [options.maxBuffer=10485760] - maximum buffer size for `stdout` and `stderr` +* @param {Callback} clbk - callback to invoke upon completion +* @throws {TypeError} options argument must be an object +* @throws {TypeError} must provide valid options +*/ +function cdoctest( options, clbk ) { + var results; + var opts; + var err; + + opts = {}; + opts.compiler = DEFAULTS.compiler; + opts.flags = DEFAULTS.flags.slice(); + opts.libraries = DEFAULTS.libraries.slice(); + opts.include = DEFAULTS.include.slice(); + opts.libpath = DEFAULTS.libpath.slice(); + opts.sourceFiles = DEFAULTS.sourceFiles.slice(); + opts.verbose = DEFAULTS.verbose; + opts.timeout = DEFAULTS.timeout; + opts.maxBuffer = DEFAULTS.maxBuffer; + + err = validate( opts, options ); + if ( err ) { + throw err; + } + + results = { + 'total': 0, + 'pass': 0, + 'fail': 0, + 'errors': [] + }; + + processFiles( opts.files, 0 ); + + /** + * Processes files sequentially. + * + * @private + * @param {StringArray} files - file list + * @param {number} idx - current index + */ + function processFiles( files, idx ) { + var resolved; + var examples; + var content; + var fpath; + + if ( idx >= files.length ) { + return clbk( null, results ); + } + fpath = resolve( files[ idx ] ); + debug( 'Processing file: %s', fpath ); + + try { + content = readFileSync( fpath, 'utf8' ); + } catch ( err ) { + debug( 'Unable to read file: %s. Error: %s', fpath, err.message ); + results.errors.push({ + 'file': fpath, + 'error': err.message + }); + return processFiles( files, idx + 1 ); + } + + // Skip Node-API related files: + if ( + content.indexOf( '#include ' ) !== -1 || + content.indexOf( '#include ' ) !== -1 || + content.indexOf( '#include ' ) !== -1 || + content.indexOf( '#include ' ) !== -1 + ) { + debug( 'Skipping Node-API related file: %s', fpath ); + return processFiles( files, idx + 1 ); + } + + // Auto-resolve include directories, source files, libraries, and library paths from the package manifest: + resolved = resolveIncludes( content, fpath ); + debug( 'Auto-resolved %d include directories for %s.', resolved.includes.length, fpath ); + debug( 'Auto-resolved %d source files for %s.', resolved.sources.length, fpath ); + debug( 'Auto-resolved %d libraries for %s.', resolved.libraries.length, fpath ); + debug( 'Auto-resolved %d library paths for %s.', resolved.libpath.length, fpath ); + + // Extract @example blocks from the source file: + examples = extractExamples( content, fpath ); + debug( 'Found %d @example blocks in %s.', examples.length, fpath ); + + if ( examples.length === 0 ) { + return processFiles( files, idx + 1 ); + } + processExamples( examples, fpath, content, resolved, 0, function onDone( ) { + processFiles( files, idx + 1 ); + }); + } + + /** + * Processes examples from a single file. + * + * @private + * @param {Array} examples - array of example objects + * @param {string} fpath - file path + * @param {string} sourceContent - original source file content + * @param {Object} resolved - auto-resolved manifest data + * @param {StringArray} resolved.includes - include directories + * @param {StringArray} resolved.sources - source files + * @param {StringArray} resolved.libraries - libraries + * @param {StringArray} resolved.libpath - library paths + * @param {number} idx - current index + * @param {Function} done - callback + */ + function processExamples( examples, fpath, sourceContent, resolved, idx, done ) { + var result; + var tmpDir; + var srcFile; + var outFile; + var example; + var dir; + var cmd; + + if ( idx >= examples.length ) { + return done( ); + } + example = examples[ idx ]; + results.total += 1; + + debug( 'Processing example %d of %d from %s (line %d)...', idx + 1, examples.length, fpath, example.line ); + + if ( opts.verbose ) { + console.log( '\n---\nRunning doctest from %s (line %d)...\n', fpath, example.line ); // eslint-disable-line no-console + } + + dir = dirname( fpath ); + + // Use Approach A (append-to-source) for all files. This ensures that: + // 1. We can test internal `static` functions. + // 2. We avoid `multiple definition` errors at link time (which occur + // in Normal mode when headers define data symbols and are included + // in both the source file and the doctest file). + // 3. We have a simpler, more robust unified pipeline. + var useAppendMode = true; + + // Generate a test program from the example: + var genOpts = {}; + + // Automatically discover the package's own header files + // (source files typically don't #include their own package header, + // so the generated doctest code would lack a declaration for the + // function under test — GCC then assumes int return type → wrong results): + var pkgHeaders = findPackageHeaders( fpath, resolved.includes ); + + if ( useAppendMode ) { + genOpts.appendMode = true; + } + if ( pkgHeaders.length > 0 ) { + genOpts.extraIncludes = pkgHeaders; + } + result = generateTestProgram( example, fpath, sourceContent, genOpts ); + + // Create a temporary directory: + tmpDir = join( dir, '__tmp_c_doctest_' + now( ).toString( ) + '__' ); + try { + mkdirSync( tmpDir, { 'recursive': true } ); + } catch ( err ) { + results.fail += 1; + results.errors.push({ + 'file': fpath, + 'line': example.line, + 'error': format( 'Unable to create temporary directory. Error: %s', err.message ) + }); + return processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + + outFile = join( tmpDir, 'doctest.out' ); + + if ( useAppendMode ) { + // Approach A: append doctest main() to a temp copy of the source file, + // then compile the temp copy in place of the original: + srcFile = join( tmpDir, 'doctest_main.c' ); + try { + // We use #line directives so that GCC reports errors relative to the original source file: + var composite = '#line 1 "' + fpath + '"\n' + sourceContent + + '\n#line ' + ( example.line + 1 ) + ' "' + fpath + '"\n' + + result.program; + writeFileSync( srcFile, composite ); + } catch ( err ) { + cleanup( tmpDir, srcFile, outFile ); + results.fail += 1; + results.errors.push({ + 'file': fpath, + 'line': example.line, + 'error': format( 'Unable to write temporary file. Error: %s', err.message ) + }); + return processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + // Build compile command, replacing the original source with the temp copy: + cmd = buildCompileCommand( null, outFile, resolved, fpath, srcFile ); + } else { + // Normal mode: separate doctest.c file: + srcFile = join( tmpDir, 'doctest.c' ); + try { + writeFileSync( srcFile, result.program ); + } catch ( err ) { + cleanup( tmpDir, srcFile, outFile ); + results.fail += 1; + results.errors.push({ + 'file': fpath, + 'line': example.line, + 'error': format( 'Unable to write temporary file. Error: %s', err.message ) + }); + return processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + cmd = buildCompileCommand( srcFile, outFile, resolved ); + } + debug( 'Compile command: %s', cmd ); + + exec( cmd, { + 'cwd': dir, + 'maxBuffer': opts.maxBuffer, + 'timeout': opts.timeout + }, onCompile ); + + /** + * Callback invoked upon completing compilation. + * + * @private + * @param {Error} error - error object + * @param {Buffer} stdout - standard output + * @param {Buffer} stderr - standard error + */ + function onCompile( error, stdout, stderr ) { + var filteredErr; + if ( error ) { + filteredErr = filterStderr( ( stderr || '' ).toString( ), fpath ); + debug( 'Compilation failed for example at line %d in %s.', example.line, fpath ); + cleanup( tmpDir, srcFile, outFile ); + results.fail += 1; + results.errors.push({ + 'file': fpath, + 'line': example.line, + 'error': format( 'Compilation failed.\n\n%s', filteredErr || stderr.toString( ) ) + }); + return processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + debug( 'Compilation succeeded. Running executable...' ); + + // Even if compilation succeeded, there might be relevant warnings: + filteredErr = filterStderr( ( stderr || '' ).toString( ), fpath ); + if ( filteredErr && opts.verbose ) { + console.error( filteredErr ); // eslint-disable-line no-console + } + + exec( outFile, { + 'cwd': dir, + 'maxBuffer': opts.maxBuffer, + 'timeout': opts.timeout + }, onRun ); + } + + /** + * Callback invoked upon completing execution. + * + * @private + * @param {Error} error - error object + * @param {Buffer} stdout - standard output + * @param {Buffer} stderr - standard error + */ + function onRun( error, stdout, stderr ) { + var outputResults; + var actual; + var lines; + var out; + var msg; + var i; + + cleanup( tmpDir, srcFile, outFile ); + + out = ( stdout || '' ).toString( ); + + if ( error ) { + debug( 'Execution failed for example at line %d in %s.', example.line, fpath ); + results.fail += 1; + results.errors.push({ + 'file': fpath, + 'line': example.line, + 'error': format( 'Execution failed.\n\n%s', ( stderr || '' ).toString( ) || error.message ) + }); + return processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + + // Parse output for doctest result lines: + lines = out.split( '\n' ); + outputResults = []; + for ( i = 0; i < lines.length; i++ ) { + if ( lines[ i ].indexOf( RESULT_TAG ) === 0 ) { + outputResults.push( lines[ i ].substring( RESULT_TAG.length ) ); + } + } + + // Compare each result with its annotation using compareValues: + for ( i = 0; i < outputResults.length && i < result.annotations.length; i++ ) { + actual = parseOutputValue( outputResults[ i ] ); + msg = compareValues( actual, result.annotations[ i ].value ); + if ( msg ) { + debug( 'Doctest assertion failed at line %d in %s: %s', result.annotations[ i ].line, fpath, msg ); + results.fail += 1; + results.errors.push({ + 'file': fpath, + 'line': result.annotations[ i ].line, + 'error': format( 'Doctest assertion failed. %s (annotation: `// returns %s`, actual: `%s`)', msg, result.annotations[ i ].value, createAnnotationValue( actual ) ) + }); + if ( opts.verbose ) { + console.log( format( 'DOCTEST FAILED: %s', msg ) ); // eslint-disable-line no-console + } + return processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + } + + debug( 'Doctest passed for example at line %d in %s.', example.line, fpath ); + results.pass += 1; + if ( opts.verbose ) { + console.log( 'DOCTEST PASSED' ); // eslint-disable-line no-console + } + processExamples( examples, fpath, sourceContent, resolved, idx + 1, done ); + } + } + + /** + * Builds a compile command. + * + * @private + * @param {(string|null)} srcFile - doctest source file path (null in append mode) + * @param {string} outFile - output file path + * @param {Object} resolved - auto-resolved manifest data + * @param {StringArray} resolved.includes - auto-resolved include directories + * @param {StringArray} resolved.sources - auto-resolved source files + * @param {StringArray} resolved.libraries - auto-resolved libraries + * @param {StringArray} resolved.libpath - auto-resolved library paths + * @param {string} [replaceSrc] - original source file path to replace (append mode) + * @param {string} [replaceWith] - temp file path to replace it with (append mode) + * @returns {string} compile command + */ + function buildCompileCommand( srcFile, outFile, resolved, replaceSrc, replaceWith ) { + var cmd; + var src; + var i; + + cmd = opts.compiler; + + for ( i = 0; i < opts.flags.length; i++ ) { + cmd += ' ' + opts.flags[ i ]; + } + // Add auto-resolved include directories first: + for ( i = 0; i < resolved.includes.length; i++ ) { + cmd += ' -I ' + resolved.includes[ i ]; + } + // Then user-provided include directories: + for ( i = 0; i < opts.include.length; i++ ) { + cmd += ' -I ' + opts.include[ i ]; + } + cmd += ' -o ' + outFile; + + // Add auto-resolved source files, replacing the original if in append mode: + for ( i = 0; i < resolved.sources.length; i++ ) { + src = resolved.sources[ i ]; + // Use resolve() to normalize both paths so relative vs absolute + // comparison doesn't fail (Bug 2 fix): + if ( replaceSrc && resolve( src ) === resolve( replaceSrc ) ) { + // In append mode, swap the original source for the temp copy: + cmd += ' ' + replaceWith; + } else { + cmd += ' ' + src; + } + } + // Then user-provided source files: + for ( i = 0; i < opts.sourceFiles.length; i++ ) { + cmd += ' ' + opts.sourceFiles[ i ]; + } + // In normal mode, add the doctest source file: + if ( srcFile ) { + cmd += ' ' + srcFile; + } + + // Add auto-resolved library paths first: + for ( i = 0; i < resolved.libpath.length; i++ ) { + cmd += ' -L ' + resolved.libpath[ i ]; + } + // Then user-provided library paths: + for ( i = 0; i < opts.libpath.length; i++ ) { + cmd += ' -L ' + opts.libpath[ i ]; + } + // Add auto-resolved libraries first: + for ( i = 0; i < resolved.libraries.length; i++ ) { + cmd += ' ' + resolved.libraries[ i ]; + } + // Then user-provided libraries: + for ( i = 0; i < opts.libraries.length; i++ ) { + cmd += ' ' + opts.libraries[ i ]; + } + return cmd; + } +} + +/** +* Parses a C program output value string into a JavaScript value. +* +* @private +* @param {string} str - output value string from C printf +* @returns {number} parsed JavaScript value +*/ +function parseOutputValue( str ) { + var lower = str.trim( ).toLowerCase( ); + if ( lower === 'nan' || lower === '-nan' ) { + return NaN; + } + if ( lower === 'inf' || lower === '+inf' || lower === 'infinity' ) { + return Infinity; + } + if ( lower === '-inf' || lower === '-infinity' ) { + return -Infinity; // eslint-disable-line no-loss-of-precision + } + // [Fix 3] Handle bool outputs printed as "true"/"false": + if ( lower === 'true' ) { + return true; + } + if ( lower === 'false' ) { + return false; + } + return parseFloat( str.trim( ) ); +} + +/** +* Cleans up temporary files. +* +* @private +* @param {string} tmpDir - temporary directory +* @param {string} srcFile - source file path +* @param {string} outFile - output file path +*/ +function cleanup( tmpDir, srcFile, outFile ) { + try { + unlinkSync( srcFile ); + } catch ( err ) { + debug( 'Unable to remove temporary source file: %s', srcFile ); + } + try { + unlinkSync( outFile ); + } catch ( err ) { + debug( 'Unable to remove temporary output file: %s', outFile ); + } + try { + rmdirSync( tmpDir ); + } catch ( err ) { + debug( 'Unable to remove temporary directory: %s', tmpDir ); + } +} + +/** +* Checks whether a C source file contains `static` function definitions that +* have `@example` JSDoc blocks. If so, the file should use append-to-source +* mode (Approach A) for doctesting so that static functions are accessible. +* +* @private +* @param {string} content - source file content +* @returns {boolean} true if the file has static functions with @example blocks +*/ +function hasStaticExamples( content ) { + var inComment; + var hasExample; + var lines; + var line; + var i; + + lines = content.split( '\n' ); + inComment = false; + hasExample = false; + + for ( i = 0; i < lines.length; i++ ) { + line = lines[ i ]; + + // Track JSDoc comment blocks: + if ( /\/\*\*/.test( line ) ) { + inComment = true; + hasExample = false; + continue; + } + if ( inComment && /\*\//.test( line ) ) { + inComment = false; + // The line after `*/` is the function definition. + // Check the next non-empty line for `static`: + if ( hasExample ) { + // Look ahead for the function definition: + for ( var j = i + 1; j < lines.length && j <= i + 3; j++ ) { + if ( RE_STATIC_FUNC.test( lines[ j ] ) ) { + return true; + } + if ( lines[ j ].trim( ).length > 0 ) { + break; // stop at first non-empty non-static line + } + } + } + hasExample = false; + continue; + } + if ( inComment && /^\s*\*\s*@example\s*$/.test( line ) ) { + hasExample = true; + } + } + return false; +} + +/** +* Filters the compilation output to suppress noise from dependency files. +* +* @private +* @param {string} stderr - compilation output +* @param {string} fpath - path to the file under test +* @returns {string} filtered output +*/ +function filterStderr( stderr, fpath ) { + var filtered; + var lines; + var i; + + if ( !stderr ) { + return ''; + } + lines = stderr.split( '\n' ); + filtered = []; + + for ( i = 0; i < lines.length; i++ ) { + // Keep lines that mention the target file (absolute path mapping from #line): + if ( lines[ i ].indexOf( fpath ) !== -1 ) { + filtered.push( lines[ i ] ); + // Look ahead to collect associated context lines (code snippets, carats, etc.): + while ( i + 1 < lines.length && + ( lines[ i + 1 ].indexOf( '|' ) !== -1 || + lines[i + 1].indexOf('^') !== -1 || + lines[i + 1].trim().length === 0 ) ) { + i += 1; + filtered.push( lines[ i ] ); + } + } else if ( lines[ i ].indexOf( 'error:' ) !== -1 ) { + // Keep all errors (they block the build), but only if they are not from a redundant source: + filtered.push( lines[ i ] ); + while ( i + 1 < lines.length && + ( lines[ i + 1 ].indexOf( '|' ) !== -1 || + lines[i + 1].indexOf('^') !== -1 || + lines[i + 1].trim().length === 0) ) { + i += 1; + filtered.push( lines[ i ] ); + } + } + } + return filtered.join( '\n' ).trim(); +} + +/** +* Finds a package's own header files. +* +* @private +* @param {string} fpath - source file path +* @param {StringArray} includes - include directories +* @returns {StringArray} header files (as #include directives) +*/ +function findPackageHeaders( fpath, includes ) { + var headers; + var dir; + + headers = []; + if ( includes.length === 0 ) { + return headers; + } + // The first include directory is typically the package's own `include/` directory: + dir = includes[ 0 ]; + if ( !existsSync( dir ) ) { + return headers; + } + + function walk( currentDir, relativePath ) { + var stats; + var files; + var i; + + try { + files = readdirSync( currentDir ); + } catch ( err ) { + return; + } + for ( i = 0; i < files.length; i++ ) { + stats = statSync( join( currentDir, files[ i ] ) ); + if ( stats.isDirectory() ) { + walk( join( currentDir, files[ i ] ), join( relativePath, files[ i ] ) ); + } else if ( /\.h$/.test( files[ i ] ) ) { + headers.push( '#include "' + join( relativePath, files[ i ] ) + '"' ); + } + } + } + + walk( dir, '' ); + return headers; +} + + +// EXPORTS // + +module.exports = cdoctest; diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/resolve_includes.js b/lib/node_modules/@stdlib/_tools/doctest/c/lib/resolve_includes.js new file mode 100644 index 000000000000..dea2e3863718 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/resolve_includes.js @@ -0,0 +1,553 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var path = require( 'path' ); +var existsSync = require( 'fs' ).existsSync; +var readFileSync = require( 'fs' ).readFileSync; +var writeFileSync = require( 'fs' ).writeFileSync; +var unlinkSync = require( 'fs' ).unlinkSync; +var logger = require( 'debug' ); +var dirname = require( '@stdlib/utils/dirname' ); +var manifest = require( '@stdlib/utils/library-manifest' ); + + +// VARIABLES // + +var debug = logger( 'c-doctest:resolve-includes' ); + +// Default manifest filename: +var MANIFEST_FILENAME = 'manifest.json'; + +// Regular expression to match #include directives: +var RE_INCLUDE = /^\s*#include\s+[<"]([^>"]+)[>"]/; + + +// FUNCTIONS // + +/** +* Extracts `@stdlib` package names from C include directives. +* +* @private +* @param {string} code - C source code +* @returns {StringArray} list of package names inferred from include directives +*/ +function extractHeaderPackages( code ) { + var headers; + var line; + var out; + var pkg; + var m; + var i; + + out = []; + headers = {}; + + code = code.split( '\n' ); + for ( i = 0; i < code.length; i++ ) { + line = code[ i ].trim(); + // Handle include directives inside JSDoc example lines: + if ( line.charAt( 0 ) === '*' ) { + line = line.substring( 1 ).trim(); + } + m = RE_INCLUDE.exec( line ); + if ( !m ) { + continue; + } + if ( m[ 1 ].indexOf( 'stdlib/' ) !== 0 ) { + continue; + } + if ( m[ 1 ].slice( -2 ) !== '.h' ) { + continue; + } + pkg = '@' + m[ 1 ].slice( 0, -2 ); + if ( !headers[ pkg ] ) { + headers[ pkg ] = true; + out.push( pkg ); + } + } + return out; +} + +/** +* Reads and parses a manifest file. +* +* @private +* @param {string} manifestPath - manifest path +* @returns {(Object|null)} manifest object or null +*/ +function readManifest( manifestPath ) { + var data; + + try { + data = readFileSync( manifestPath, 'utf8' ); + return JSON.parse( data ); + } catch ( err ) { + debug( 'Unable to parse manifest at %s. Error: %s', manifestPath, err.message ); + return null; + } +} + +/** +* Selects manifest configurations relevant to a task. +* +* @private +* @param {Object} data - manifest object +* @param {string} task - task name +* @returns {Array} list of selected configurations +*/ +function selectManifestConfs( data, task ) { + var generic; + var out; + var i; + + if ( !data || !data.confs || data.confs.length === 0 ) { + return []; + } + out = []; + generic = []; + + for ( i = 0; i < data.confs.length; i++ ) { + if ( data.confs[ i ].task === task ) { + out.push( data.confs[ i ] ); + } else if ( data.confs[ i ].task === void 0 ) { + generic.push( data.confs[ i ] ); + } + } + if ( out.length > 0 ) { + return out; + } + if ( generic.length > 0 ) { + return generic; + } + return [ data.confs[ 0 ] ]; +} + +/** +* Collects declared manifest dependencies for a given task. +* +* @private +* @param {string} manifestPath - manifest path +* @param {string} task - task name +* @returns {StringArray} dependency list +*/ +function collectManifestDependencies( manifestPath, task ) { + var dependencies; + var confs; + var data; + var out; + var i; + var j; + + data = readManifest( manifestPath ); + if ( !data ) { + return []; + } + confs = selectManifestConfs( data, task ); + dependencies = {}; + out = []; + + for ( i = 0; i < confs.length; i++ ) { + if ( !confs[ i ].dependencies ) { + continue; + } + for ( j = 0; j < confs[ i ].dependencies.length; j++ ) { + if ( !dependencies[ confs[ i ].dependencies[ j ] ] ) { + dependencies[ confs[ i ].dependencies[ j ] ] = true; + out.push( confs[ i ].dependencies[ j ] ); + } + } + } + return out; +} + +/** +* Returns a package name for a package root path. +* +* @private +* @param {string} pkgRoot - package root path +* @returns {(string|null)} package name +*/ +function packageName( pkgRoot ) { + var pkgPath; + var data; + + pkgPath = path.join( pkgRoot, 'package.json' ); + if ( !existsSync( pkgPath ) ) { + return null; + } + try { + data = JSON.parse( readFileSync( pkgPath, 'utf8' ) ); + return data.name || null; + } catch ( err ) { + debug( 'Unable to parse package metadata at %s. Error: %s', pkgPath, err.message ); + return null; + } +} + +/** +* Resolves a package directory path from a package name. +* +* @private +* @param {string} pkgName - package name +* @param {string} pkgRoot - package root path +* @returns {(string|null)} package directory path or null +*/ +function packagePath( pkgName, pkgRoot ) { + var suffix; + var idx; + var dir; + + suffix = '/lib/node_modules/'; + idx = pkgRoot.indexOf( suffix ); + if ( idx === -1 ) { + return null; + } + dir = pkgRoot.substring( 0, idx + suffix.length ); + return path.join( dir, pkgName ); +} + +/** +* Filters package dependencies to dependencies resolvable as local stdlib packages. +* +* @private +* @param {StringArray} dependencies - inferred dependencies +* @param {string} pkgRoot - package root path +* @param {(string|null)} self - current package name +* @returns {StringArray} filtered dependencies +*/ +function filterDependencies( dependencies, pkgRoot, self ) { + var manifestPath; + var seen; + var dpath; + var out; + var i; + + seen = {}; + out = []; + + for ( i = 0; i < dependencies.length; i++ ) { + if ( dependencies[ i ] === self ) { + continue; + } + dpath = packagePath( dependencies[ i ], pkgRoot ); + if ( !dpath ) { + continue; + } + manifestPath = path.join( dpath, MANIFEST_FILENAME ); + if ( existsSync( manifestPath ) && !seen[ dependencies[ i ] ] ) { + seen[ dependencies[ i ] ] = true; + out.push( dependencies[ i ] ); + } + } + return out; +} + +/** +* Computes dependencies missing from manifest dependencies. +* +* @private +* @param {StringArray} inferred - inferred dependencies +* @param {StringArray} declared - declared dependencies +* @returns {StringArray} missing dependencies +*/ +function missingDependencies( inferred, declared ) { + var seen; + var out; + var i; + + seen = {}; + out = []; + for ( i = 0; i < declared.length; i++ ) { + seen[ declared[ i ] ] = true; + } + for ( i = 0; i < inferred.length; i++ ) { + if ( !seen[ inferred[ i ] ] ) { + out.push( inferred[ i ] ); + } + } + return out; +} + +/** +* Creates a temporary manifest file with additional dependencies. +* +* @private +* @param {string} manifestPath - manifest path +* @param {string} task - task name +* @param {StringArray} dependencies - dependencies to add +* @returns {(string|null)} temporary manifest path or null +*/ +function createTempManifest( manifestPath, task, dependencies ) { + var tmpPath; + var confs; + var data; + var i; + var j; + + if ( dependencies.length === 0 ) { + return null; + } + data = readManifest( manifestPath ); + if ( !data ) { + return null; + } + confs = selectManifestConfs( data, task ); + for ( i = 0; i < confs.length; i++ ) { + if ( !confs[ i ].dependencies ) { + confs[ i ].dependencies = []; + } + for ( j = 0; j < dependencies.length; j++ ) { + if ( confs[ i ].dependencies.indexOf( dependencies[ j ] ) === -1 ) { + confs[ i ].dependencies.push( dependencies[ j ] ); + } + } + } + tmpPath = path.join( dirname( manifestPath ), '__tmp_manifest_' + Date.now().toString() + '__.json' ); + try { + writeFileSync( tmpPath, JSON.stringify( data, null, 2 ) ); + } catch ( err ) { + debug( 'Unable to write temporary manifest. Error: %s', err.message ); + return null; + } + return tmpPath; +} + +/** +* Resolves a manifest configuration for a task, optionally augmenting dependencies. +* +* @private +* @param {string} manifestPath - manifest path +* @param {string} pkgRoot - package root +* @param {string} task - task name +* @param {StringArray} dependencies - dependencies to augment +* @returns {Object} resolved configuration +*/ +function resolveManifest( manifestPath, pkgRoot, task, dependencies ) { + var tmpManifestPath; + var targetManifest; + var conf; + + tmpManifestPath = createTempManifest( manifestPath, task, dependencies ); + targetManifest = tmpManifestPath || manifestPath; + + try { + conf = manifest( targetManifest, { + 'task': task + }, { + 'basedir': pkgRoot, + 'paths': 'posix' + }); + } finally { + if ( tmpManifestPath ) { + try { + unlinkSync( tmpManifestPath ); + } catch ( err ) { + debug( 'Unable to remove temporary manifest: %s. Error: %s', tmpManifestPath, err.message ); + } + } + } + return conf; +} + + +// MAIN // + +/** +* Resolves include directories and source files from a C source file using the package's `manifest.json` and `@stdlib/utils/library-manifest`. +* +* ## Notes +* +* - Uses the existing `library-manifest` utility to recursively resolve all transitive dependencies, include directories, source files, libraries, and library paths. +* - Falls back to a basic resolution (just the source file) if no `manifest.json` is found. +* +* @param {string} content - source file content (unused; kept for API compatibility) +* @param {string} fpath - source file path +* @returns {Object} object with `includes`, `sources`, `libraries`, and `libpath` arrays +*/ +function resolveIncludes( content, fpath ) { + var inferredDependencies; + var declaredDependencies; + var dependenciesToAdd; + var self; + var manifestPath; + var pkgRoot; + var conf; + var out; + var i; + + out = { + 'includes': [], + 'sources': [], + 'libraries': [], + 'libpath': [] + }; + + // Find the package root (directory containing package.json): + pkgRoot = findPackageRoot( fpath ); + if ( !pkgRoot ) { + debug( 'Unable to find package root for: %s. Using file as sole source.', fpath ); + out.sources.push( fpath ); + return out; + } + debug( 'Found package root: %s', pkgRoot ); + self = packageName( pkgRoot ); + + // Check for a manifest.json: + manifestPath = path.join( pkgRoot, MANIFEST_FILENAME ); + if ( !existsSync( manifestPath ) ) { + debug( 'No manifest.json found at: %s. Using basic resolution.', manifestPath ); + out.sources.push( fpath ); + + // Still try to add the package's own include directory: + if ( existsSync( path.join( pkgRoot, 'include' ) ) ) { + out.includes.push( path.join( pkgRoot, 'include' ) ); + } + return out; + } + debug( 'Found manifest: %s', manifestPath ); + + // Infer dependencies from stdlib headers in source and augment missing dependencies: + inferredDependencies = extractHeaderPackages( content ); + declaredDependencies = collectManifestDependencies( manifestPath, 'examples' ); + dependenciesToAdd = missingDependencies( inferredDependencies, declaredDependencies ); + dependenciesToAdd = filterDependencies( dependenciesToAdd, pkgRoot, self ); + if ( dependenciesToAdd.length > 0 ) { + debug( 'Temporarily augmenting manifest dependencies for task=examples: %s', dependenciesToAdd.join( ', ' ) ); + } + + // Use library-manifest to resolve all dependencies recursively. + // Use the "examples" task if available, falling back to "build": + try { + conf = resolveManifest( manifestPath, pkgRoot, 'examples', dependenciesToAdd ); + } catch ( err ) { + debug( 'Failed to resolve manifest with task=examples: %s. Trying task=build.', err.message ); + declaredDependencies = collectManifestDependencies( manifestPath, 'build' ); + dependenciesToAdd = missingDependencies( inferredDependencies, declaredDependencies ); + dependenciesToAdd = filterDependencies( dependenciesToAdd, pkgRoot, self ); + if ( dependenciesToAdd.length > 0 ) { + debug( 'Temporarily augmenting manifest dependencies for task=build: %s', dependenciesToAdd.join( ', ' ) ); + } + try { + conf = resolveManifest( manifestPath, pkgRoot, 'build', dependenciesToAdd ); + } catch ( err2 ) { + debug( 'Failed to resolve manifest with task=build: %s. Using basic resolution.', err2.message ); + out.sources.push( fpath ); + if ( existsSync( path.join( pkgRoot, 'include' ) ) ) { + out.includes.push( path.join( pkgRoot, 'include' ) ); + } + return out; + } + } + + // If manifest resolution returned an empty object, fall back: + if ( !conf || ( !conf.src && !conf.include ) ) { + debug( 'Manifest resolved to empty configuration. Using basic resolution.' ); + out.sources.push( fpath ); + if ( existsSync( path.join( pkgRoot, 'include' ) ) ) { + out.includes.push( path.join( pkgRoot, 'include' ) ); + } + return out; + } + + // Resolve source files (paths from manifest are relative to pkgRoot): + if ( conf.src ) { + for ( i = 0; i < conf.src.length; i++ ) { + out.sources.push( path.resolve( pkgRoot, conf.src[ i ] ) ); + } + debug( 'Resolved %d source files via manifest.', conf.src.length ); + } + + // Resolve include directories: + if ( conf.include ) { + for ( i = 0; i < conf.include.length; i++ ) { + out.includes.push( path.resolve( pkgRoot, conf.include[ i ] ) ); + } + debug( 'Resolved %d include directories via manifest.', conf.include.length ); + } + + // Resolve libraries: + if ( conf.libraries ) { + for ( i = 0; i < conf.libraries.length; i++ ) { + out.libraries.push( conf.libraries[ i ] ); + } + } + + // Resolve library paths: + if ( conf.libpath ) { + for ( i = 0; i < conf.libpath.length; i++ ) { + out.libpath.push( path.resolve( pkgRoot, conf.libpath[ i ] ) ); + } + } + + // Filter out napi-related source files (they depend on Node.js headers): + out.sources = filterNapiSources( out.sources ); + + return out; +} + +/** +* Finds the package root directory (containing package.json) by walking up. +* +* @private +* @param {string} fpath - file path +* @returns {(string|null)} package root path or null +*/ +function findPackageRoot( fpath ) { + var dir; + var prev; + + dir = dirname( fpath ); + prev = ''; + + while ( dir !== prev ) { + if ( existsSync( path.join( dir, 'package.json' ) ) ) { + return dir; + } + prev = dir; + dir = dirname( dir ); + } + return null; +} + +/** +* Filters out napi-related source files that require Node.js headers. +* +* @private +* @param {StringArray} sources - array of source file paths +* @returns {StringArray} filtered source files +*/ +function filterNapiSources( sources ) { + var out = []; + var i; + + for ( i = 0; i < sources.length; i++ ) { + if ( sources[ i ].indexOf( '/napi/' ) === -1 ) { + out.push( sources[ i ] ); + } + } + return out; +} + + +// EXPORTS // + +module.exports = resolveIncludes; diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/lib/validate.js b/lib/node_modules/@stdlib/_tools/doctest/c/lib/validate.js new file mode 100644 index 000000000000..158e4483bd68 --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/lib/validate.js @@ -0,0 +1,132 @@ +/** +* @license Apache-2.0 +* +* Copyright (c) 2026 The Stdlib Authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +// MODULES // + +var isString = require( '@stdlib/assert/is-string' ).isPrimitive; +var isStringArray = require( '@stdlib/assert/is-string-array' ).primitives; +var isBoolean = require( '@stdlib/assert/is-boolean' ).isPrimitive; +var isPositiveInteger = require( '@stdlib/assert/is-positive-integer' ).isPrimitive; +var isObject = require( '@stdlib/assert/is-plain-object' ); +var hasOwnProp = require( '@stdlib/assert/has-own-property' ); +var format = require( '@stdlib/string/format' ); + + +// MAIN // + +/** +* Validates function options. +* +* @private +* @param {Object} opts - destination for function options +* @param {Options} options - function options +* @param {StringArray} options.files - list of source file paths to test +* @param {string} [options.compiler] - C compiler to use +* @param {Array} [options.flags] - compiler flags +* @param {Array} [options.libraries] - libraries to link +* @param {Array} [options.include] - include directories +* @param {Array} [options.libpath] - library paths +* @param {Array} [options.sourceFiles] - additional source files +* @param {boolean} [options.verbose] - verbose output +* @param {PositiveInteger} [options.timeout] - maximum time in milliseconds +* @param {PositiveInteger} [options.maxBuffer] - maximum buffer size +* @returns {(Error|null)} error or null +* +* @example +* var opts = {}; +* var options = { +* 'files': [ 'test.c' ] +* }; +* var err = validate( opts, options ); +* if ( err ) { +* throw err; +* } +*/ +function validate( opts, options ) { + if ( !isObject( options ) ) { + return new TypeError( format( 'invalid argument. Options argument must be an object. Value: `%s`.', options ) ); + } + if ( hasOwnProp( options, 'files' ) ) { + opts.files = options.files; + if ( !isStringArray( opts.files ) ) { + return new TypeError( format( 'invalid option. `%s` option must be an array of strings. Option: `%s`.', 'files', opts.files ) ); + } + } + if ( hasOwnProp( options, 'compiler' ) ) { + opts.compiler = options.compiler; + if ( !isString( opts.compiler ) ) { + return new TypeError( format( 'invalid option. `%s` option must be a string. Option: `%s`.', 'compiler', opts.compiler ) ); + } + } + if ( hasOwnProp( options, 'flags' ) ) { + opts.flags = options.flags; + if ( !isStringArray( opts.flags ) ) { + return new TypeError( format( 'invalid option. `%s` option must be an array of strings. Option: `%s`.', 'flags', opts.flags ) ); + } + } + if ( hasOwnProp( options, 'libraries' ) ) { + opts.libraries = options.libraries; + if ( !isStringArray( opts.libraries ) ) { + return new TypeError( format( 'invalid option. `%s` option must be an array of strings. Option: `%s`.', 'libraries', opts.libraries ) ); + } + } + if ( hasOwnProp( options, 'include' ) ) { + opts.include = options.include; + if ( !isStringArray( opts.include ) ) { + return new TypeError( format( 'invalid option. `%s` option must be an array of strings. Option: `%s`.', 'include', opts.include ) ); + } + } + if ( hasOwnProp( options, 'libpath' ) ) { + opts.libpath = options.libpath; + if ( !isStringArray( opts.libpath ) ) { + return new TypeError( format( 'invalid option. `%s` option must be an array of strings. Option: `%s`.', 'libpath', opts.libpath ) ); + } + } + if ( hasOwnProp( options, 'sourceFiles' ) ) { + opts.sourceFiles = options.sourceFiles; + if ( !isStringArray( opts.sourceFiles ) ) { + return new TypeError( format( 'invalid option. `%s` option must be an array of strings. Option: `%s`.', 'sourceFiles', opts.sourceFiles ) ); + } + } + if ( hasOwnProp( options, 'verbose' ) ) { + opts.verbose = options.verbose; + if ( !isBoolean( opts.verbose ) ) { + return new TypeError( format( 'invalid option. `%s` option must be a boolean. Option: `%s`.', 'verbose', opts.verbose ) ); + } + } + if ( hasOwnProp( options, 'timeout' ) ) { + opts.timeout = options.timeout; + if ( !isPositiveInteger( opts.timeout ) ) { + return new TypeError( format( 'invalid option. `%s` option must be a positive integer. Option: `%s`.', 'timeout', opts.timeout ) ); + } + } + if ( hasOwnProp( options, 'maxBuffer' ) ) { + opts.maxBuffer = options.maxBuffer; + if ( !isPositiveInteger( opts.maxBuffer ) ) { + return new TypeError( format( 'invalid option. `%s` option must be a positive integer. Option: `%s`.', 'maxBuffer', opts.maxBuffer ) ); + } + } + return null; +} + + +// EXPORTS // + +module.exports = validate; diff --git a/lib/node_modules/@stdlib/_tools/doctest/c/package.json b/lib/node_modules/@stdlib/_tools/doctest/c/package.json new file mode 100644 index 000000000000..1924b466998b --- /dev/null +++ b/lib/node_modules/@stdlib/_tools/doctest/c/package.json @@ -0,0 +1,47 @@ +{ + "name": "@stdlib/_tools/doctest/c", + "version": "0.0.0", + "description": "C documentation test runner.", + "license": "Apache-2.0", + "author": { + "name": "The Stdlib Authors", + "url": "https://github.com/stdlib-js/stdlib/graphs/contributors" + }, + "contributors": [ + { + "name": "The Stdlib Authors", + "url": "https://github.com/stdlib-js/stdlib/graphs/contributors" + } + ], + "main": "./lib", + "bin": { + "c-doctest": "./bin/cli" + }, + "directories": { + "lib": "./lib", + "doc": "./docs" + }, + "scripts": {}, + "homepage": "https://github.com/stdlib-js/stdlib", + "repository": { + "type": "git", + "url": "git://github.com/stdlib-js/stdlib.git" + }, + "bugs": { + "url": "https://github.com/stdlib-js/stdlib/issues" + }, + "dependencies": {}, + "devDependencies": {}, + "engines": { + "node": ">=6.0.0" + }, + "keywords": [ + "stdlib", + "tools", + "doctest", + "c", + "test", + "documentation", + "examples" + ] +} diff --git a/tools/make/lib/examples/Makefile b/tools/make/lib/examples/Makefile index 48ca67464813..0d94ff3a279f 100644 --- a/tools/make/lib/examples/Makefile +++ b/tools/make/lib/examples/Makefile @@ -21,6 +21,7 @@ # Note: keep the following in alphabetical order... include $(TOOLS_MAKE_LIB_DIR)/examples/c.mk include $(TOOLS_MAKE_LIB_DIR)/examples/cpp.mk +include $(TOOLS_MAKE_LIB_DIR)/examples/doctest_c.mk include $(TOOLS_MAKE_LIB_DIR)/examples/javascript.mk include $(TOOLS_MAKE_LIB_DIR)/examples/markdown.mk @@ -59,7 +60,7 @@ examples: examples-javascript # @example # make examples-lang EXAMPLES_FILTER=".*/blas/base/dasum/.*" #/ -examples-lang: examples-javascript examples-c examples-cpp +examples-lang: examples-javascript examples-c examples-cpp doctest-c .PHONY: examples-lang diff --git a/tools/make/lib/examples/doctest_c.mk b/tools/make/lib/examples/doctest_c.mk new file mode 100644 index 000000000000..31a3c5199b9b --- /dev/null +++ b/tools/make/lib/examples/doctest_c.mk @@ -0,0 +1,140 @@ +#/ +# @license Apache-2.0 +# +# Copyright (c) 2026 The Stdlib Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#/ + +# VARIABLES # + +# Define the path to the C doctest CLI: +C_DOCTEST_BIN ?= $(TOOLS_PKGS_DIR)/doctest/c/bin/cli + +# Define default include paths for C doctesting: +C_DOCTEST_INCLUDE ?= $(SRC_DIR) + +# Define default C compiler for doctesting: +C_DOCTEST_COMPILER ?= gcc + +# Define output log file for doctest failures: +C_DOCTEST_FAIL_LOG ?= $(ROOT_DIR)/FAIL_LOG_DOCTEST_DOUNRCE + + +# RULES # + +#/ +# Runs C doctests extracted from `@example` annotations in C source files. +# +# ## Notes +# +# - This rule finds C source files containing `@example` annotations and verifies them by compiling and running them. +# - This rule is useful when wanting to glob for C source files (e.g., run all C doctests for a particular package). +# +# +# @param {string} [SOURCES_FILTER] - file path pattern (e.g., `.*/math/base/special/abs/.*`) +# +# @example +# make doctest-c +# +# @example +# make doctest-c SOURCES_FILTER=".*/math/base/special/abs/.*" +#/ +doctest-c: $(NODE_MODULES) + $(QUIET) fail_log="$(C_DOCTEST_FAIL_LOG)"; \ + : > "$$fail_log"; \ + fail_marker="$${fail_log}.marker"; \ + rm -f "$$fail_marker"; \ + $(FIND_C_SOURCES_CMD) | grep '^[\/]\|^[a-zA-Z]:[/\]' | grep -v '/_tools/' | xargs grep -l '@example' 2>/dev/null | while read -r file; do \ + echo ""; \ + echo "Running C doctest: $$file"; \ + tmp_log="$${fail_log}.tmp"; \ + status=0; \ + NODE_ENV="$(NODE_ENV_EXAMPLES)" \ + NODE_PATH="$(NODE_PATH_EXAMPLES)" \ + $(NODE) $(C_DOCTEST_BIN) \ + --compiler $(C_DOCTEST_COMPILER) \ + --include $(C_DOCTEST_INCLUDE) \ + "$$file" > "$$tmp_log" 2>&1 || status=$$?; \ + cat "$$tmp_log"; \ + if [ $$status -ne 0 ]; then \ + touch "$$fail_marker"; \ + { \ + echo "=== $$file ==="; \ + cat "$$tmp_log"; \ + echo ""; \ + } >> "$$fail_log"; \ + echo "Logged failure for $$file to $$fail_log"; \ + fi; \ + rm -f "$$tmp_log"; \ + done; \ + if [ -f "$$fail_marker" ]; then \ + echo ""; \ + echo "C doctest failures were logged to $$fail_log"; \ + rm -f "$$fail_marker"; \ + exit 1; \ + fi; \ + rm -f "$$fail_log" + +.PHONY: doctest-c + +#/ +# Runs C doctests extracted from `@example` annotations in a specified list of C source files. +# +# ## Notes +# +# - This rule is useful when wanting to run C doctests from a list of C source files generated by some other command (e.g., a list of changed C source files obtained via `git diff`). +# +# +# @param {string} FILES - list of C source file paths +# +# @example +# make doctest-c-files FILES='/foo/src/main.c /bar/src/main.c' +#/ +doctest-c-files: $(NODE_MODULES) + $(QUIET) fail_log="$(C_DOCTEST_FAIL_LOG)"; \ + : > "$$fail_log"; \ + fail_marker="$${fail_log}.marker"; \ + rm -f "$$fail_marker"; \ + for file in $(FILES); do \ + echo ""; \ + echo "Running C doctest: $$file"; \ + tmp_log="$${fail_log}.tmp"; \ + status=0; \ + NODE_ENV="$(NODE_ENV_EXAMPLES)" \ + NODE_PATH="$(NODE_PATH_EXAMPLES)" \ + $(NODE) $(C_DOCTEST_BIN) \ + --compiler $(C_DOCTEST_COMPILER) \ + --include $(C_DOCTEST_INCLUDE) \ + "$$file" > "$$tmp_log" 2>&1 || status=$$?; \ + cat "$$tmp_log"; \ + if [ $$status -ne 0 ]; then \ + touch "$$fail_marker"; \ + { \ + echo "=== $$file ==="; \ + cat "$$tmp_log"; \ + echo ""; \ + } >> "$$fail_log"; \ + echo "Logged failure for $$file to $$fail_log"; \ + fi; \ + rm -f "$$tmp_log"; \ + done; \ + if [ -f "$$fail_marker" ]; then \ + echo ""; \ + echo "C doctest failures were logged to $$fail_log"; \ + rm -f "$$fail_marker"; \ + exit 1; \ + fi; \ + rm -f "$$fail_log" + +.PHONY: doctest-c-files