diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a58d871 --- /dev/null +++ b/.clang-format @@ -0,0 +1,15 @@ +BasedOnStyle: Google +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +BinPackArguments: false +BinPackParameters: false +ColumnLimit: 100 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +IndentWidth: 4 +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +SpaceBeforeAssignmentOperators: true +Standard: Cpp11 +UseTab: Never \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..2c2d60b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,85 @@ +{ + "root": true, + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "ignorePatterns": "lib/**/*", + "env": { + "node": true, + "mocha": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2019 + }, + "plugins": [ + "@typescript-eslint", + "prettier" + ], + "rules": { + "no-restricted-properties": [ + "error", + { + "object": "describe", + "property": "only" + }, + { + "object": "it", + "property": "only" + }, + { + "object": "context", + "property": "only" + } + ], + "prettier/prettier": "error", + "no-console": "error", + "valid-typeof": "error", + "eqeqeq": [ + "error", + "always", + { + "null": "ignore" + } + ], + "strict": [ + "error", + "global" + ], + "no-restricted-syntax": [ + "error", + { + "selector": "TSEnumDeclaration", + "message": "Do not declare enums" + }, + { + "selector": "BinaryExpression[operator=/[=!]==/] Identifier[name='undefined']", + "message": "Do not strictly check undefined" + }, + { + "selector": "BinaryExpression[operator=/[=!]==/] Literal[raw='null']", + "message": "Do not strictly check null" + }, + { + "selector": "BinaryExpression[operator=/[=!]==?/] Literal[value='undefined']", + "message": "Do not strictly check typeof undefined (NOTE: currently this rule only detects the usage of 'undefined' string literal so this could be a misfire)" + } + ], + "@typescript-eslint/no-require-imports": "off" + }, + "overrides": [ + { + "files": [ + "test/**/*ts" + ], + "rules": { + // chat `expect(..)` style chaining is considered + // an unused expression + "@typescript-eslint/no-unused-expressions": "off" + } + } + ] + } \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5a0c677 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,71 @@ +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: {} + +name: Test + +jobs: + host_tests: + strategy: + matrix: + os: [macos-latest, windows-2019] + node: [16.x, 18.x, 20.x, 22.x] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + + - name: Build with Node.js ${{ matrix.node }} on ${{ matrix.os }} + run: npm install && npm run compile + shell: bash + + - name: Test ${{ matrix.os }} + shell: bash + run: npm test + + # container_tests: + # runs-on: ubuntu-latest + # strategy: + # matrix: + # linux_arch: [s390x, arm64, amd64] + # node: [16.x, 18.x, 20.x, 22.x] + # steps: + # - uses: actions/checkout@v4 + + # - uses: actions/setup-node@v4 + # with: + # node-version: ${{ matrix.node }} + + # - name: Get Full Node.js Version + # id: get_nodejs_version + # shell: bash + # run: | + # echo "version=$(node --print 'process.version.slice(1)')" >> "$GITHUB_OUTPUT" + # echo "ubuntu_version=$(node --print '(+process.version.slice(1).split(`.`).at(0)) > 16 ? `noble` : `bionic`')" >> "$GITHUB_OUTPUT" + + # - name: Set up QEMU + # uses: docker/setup-qemu-action@v3 + + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v3 + + # - name: Run Buildx + # run: | + # docker buildx create --name builder --bootstrap --use + # docker buildx build \ + # --platform linux/${{ matrix.linux_arch }} \ + # --build-arg="NODE_ARCH=${{ matrix.linux_arch == 'amd64' && 'x64' || matrix.linux_arch }}" \ + # --build-arg="NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }}" \ + # --build-arg="UBUNTU_VERSION=${{ steps.get_nodejs_version.outputs.ubuntu_version }}" \ + # --build-arg="RUN_TEST=true" \ + # --output type=local,dest=./prebuilds,platform-split=false \ + # -f ./.github/docker/Dockerfile.glibc \ + # . \ No newline at end of file diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..6f3512c --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/mocharc.json", + "require": [ + "source-map-support/register", + "test/tools/chai-addons.js" + ], + "extension": [ + "ts" + ], + "recursive": true, + "failZero": true, + "reporter": "test/tools/mongodb_reporter.js", + "color": true +} \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..169dcfb --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "arrowParens": "avoid", + "trailingComma": "none" +} \ No newline at end of file diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000..71366ce --- /dev/null +++ b/binding.gyp @@ -0,0 +1,31 @@ +{ + 'targets': [{ + 'target_name': 'zstd', + 'type': 'loadable_module', + 'defines': ['ZSTD_STATIC_LINKING_ONLY'], + 'include_dirs': [ + " + +using namespace Napi; + +Napi::String Compress(const Napi::CallbackInfo& info) { + auto string = Napi::String::New(info.Env(), "compress()"); + return string; +} +Napi::String Decompress(const Napi::CallbackInfo& info) { + auto string = Napi::String::New(info.Env(), "decompress()"); + return string; +} + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + exports.Set(Napi::String::New(env, "compress"), Napi::Function::New(env, Compress)); + exports.Set(Napi::String::New(env, "decompress"), Napi::Function::New(env, Decompress)); + return exports; +} + +NODE_API_MODULE(hello, Init) \ No newline at end of file diff --git a/test/tools/chai-addons.js b/test/tools/chai-addons.js new file mode 100644 index 0000000..3cf0974 --- /dev/null +++ b/test/tools/chai-addons.js @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable strict */ + +'use strict'; +// configure chai +const chai = require('chai'); +chai.use(require('sinon-chai')); +chai.use(require('chai-subset')); + +chai.config.truncateThreshold = 0; diff --git a/test/tools/mongodb_reporter.js b/test/tools/mongodb_reporter.js new file mode 100644 index 0000000..30026d6 --- /dev/null +++ b/test/tools/mongodb_reporter.js @@ -0,0 +1,329 @@ +/* eslint-disable no-console */ +/* eslint-disable strict */ +/* eslint-disable @typescript-eslint/no-var-requires */ +//@ts-check + +'use strict'; +const mocha = require('mocha'); +const chalk = require('chalk'); + +chalk.level = 3; + +const { + EVENT_RUN_BEGIN, + EVENT_RUN_END, + EVENT_TEST_FAIL, + EVENT_TEST_PASS, + EVENT_SUITE_BEGIN, + EVENT_SUITE_END, + EVENT_TEST_PENDING, + EVENT_TEST_BEGIN, + EVENT_TEST_END +} = mocha.Runner.constants; + +const fs = require('fs'); +const os = require('os'); + +/** + * @typedef {object} MongoMochaSuiteExtension + * @property {Date} timestamp - suite start date + * @property {string} stdout - capture of stdout + * @property {string} stderr - capture of stderr + * @property {MongoMochaTest} test - capture of stderr + * @typedef {object} MongoMochaTestExtension + * @property {Date} startTime - test start date + * @property {Date} endTime - test end date + * @property {number} elapsedTime - difference between end and start + * @property {Error} [error] - The possible error from a test + * @property {true} [skipped] - Set if test was skipped + * @typedef {MongoMochaSuiteExtension & Mocha.Suite} MongoMochaSuite + * @typedef {MongoMochaTestExtension & Mocha.Test} MongoMochaTest + */ + +// Turn this on if you have to debug this custom reporter! +let REPORT_TO_STDIO = false; + +function captureStream(stream) { + var oldWrite = stream.write; + var buf = ''; + stream.write = function (chunk) { + buf += chunk.toString(); // chunk is a String or Buffer + oldWrite.apply(stream, arguments); + }; + + return { + unhook: function unhook() { + stream.write = oldWrite; + return buf; + }, + captured: function () { + return buf; + } + }; +} + +/** + * @param {Mocha.Runner} runner + * @this {any} + */ +class MongoDBMochaReporter extends mocha.reporters.Spec { + constructor(runner) { + super(runner); + /** @type {Map} */ + this.suites = new Map(); + this.xunitWritten = false; + runner.on(EVENT_RUN_BEGIN, () => this.start()); + runner.on(EVENT_RUN_END, () => this.end()); + runner.on(EVENT_SUITE_BEGIN, suite => this.onSuite(suite)); + runner.on(EVENT_TEST_BEGIN, test => this.onTest(test)); + runner.on(EVENT_TEST_PASS, test => this.pass(test)); + runner.on(EVENT_TEST_FAIL, (test, error) => this.fail(test, error)); + runner.on(EVENT_TEST_PENDING, test => this.pending(test)); + runner.on(EVENT_SUITE_END, suite => this.suiteEnd(suite)); + runner.on(EVENT_TEST_END, test => this.testEnd(test)); + + process.on('SIGINT', () => this.end(true)); + } + start() {} + + end(ctrlC) { + try { + if (ctrlC) console.log('emergency exit!'); + const output = { testSuites: [] }; + + for (const [id, [className, { suite }]] of [...this.suites.entries()].entries()) { + let totalSuiteTime = 0; + let testCases = []; + let failureCount = 0; + + const tests = /** @type {MongoMochaTest[]}*/ (suite.tests); + for (const test of tests) { + let time = test.elapsedTime / 1000; + time = Number.isNaN(time) ? 0 : time; + + totalSuiteTime += time; + failureCount += test.state === 'failed' ? 1 : 0; + + /** @type {string | Date | number} */ + let startTime = test.startTime; + startTime = startTime ? startTime.toISOString() : 0; + + /** @type {string | Date | number} */ + let endTime = test.endTime; + endTime = endTime ? endTime.toISOString() : 0; + + let error = test.error; + let failure = error + ? { + type: error.constructor.name, + message: error.message, + stack: error.stack + } + : undefined; + + let skipped = !!test.skipped; + + testCases.push({ + name: test.title, + className, + time, + startTime, + endTime, + skipped, + failure + }); + } + + /** @type {string | Date | number} */ + let timestamp = suite.timestamp; + timestamp = timestamp ? timestamp.toISOString().split('.')[0] : ''; + + output.testSuites.push({ + package: suite.file.includes('integration') ? 'Integration' : 'Unit', + id, + name: className, + timestamp, + hostname: os.hostname(), + tests: suite.tests.length, + failures: failureCount, + errors: '0', + time: totalSuiteTime, + testCases, + stdout: suite.stdout, + stderr: suite.stderr + }); + } + + if (!this.xunitWritten) { + fs.writeFileSync('xunit.xml', outputToXML(output), { encoding: 'utf8' }); + } + this.xunitWritten = true; + console.log(chalk.bold('wrote xunit.xml')); + } catch (error) { + console.error(chalk.red(`Failed to output xunit report! ${error}`)); + } finally { + if (ctrlC) process.exit(1); + } + } + + /** + * @param {MongoMochaSuite} suite + */ + onSuite(suite) { + if (suite.root) return; + if (!this.suites.has(suite.fullTitle())) { + suite.timestamp = new Date(); + this.suites.set(suite.fullTitle(), { + suite, + stdout: captureStream(process.stdout), + stderr: captureStream(process.stderr) + }); + } else { + console.warn(`${chalk.yellow('WARNING:')} ${suite.fullTitle()} started twice`); + } + } + + /** + * @param {MongoMochaSuite} suite + */ + suiteEnd(suite) { + if (suite.root) return; + const currentSuite = this.suites.get(suite.fullTitle()); + if (!currentSuite) { + console.error('Suite never started >:('); + process.exit(1); + } + if (currentSuite.stdout || currentSuite.stderr) { + suite.stdout = currentSuite.stdout.unhook(); + suite.stderr = currentSuite.stderr.unhook(); + delete currentSuite.stdout; + delete currentSuite.stderr; + } + } + + /** + * @param {MongoMochaTest} test + */ + onTest(test) { + test.startTime = new Date(); + } + + /** + * @param {MongoMochaTest} test + */ + testEnd(test) { + test.endTime = new Date(); + test.elapsedTime = Number(test.endTime) - Number(test.startTime); + } + + /** + * @param {MongoMochaTest} test + */ + pass(test) { + if (REPORT_TO_STDIO) console.log(chalk.green(`✔ ${test.fullTitle()}`)); + } + + /** + * @param {MongoMochaTest} test + * @param {Error} error + */ + fail(test, error) { + if (REPORT_TO_STDIO) console.log(chalk.red(`⨯ ${test.fullTitle()} -- ${error.message}`)); + test.error = error; + } + + /** + * @param {MongoMochaTest & {skipReason?: string}} test + */ + pending(test) { + if (REPORT_TO_STDIO) console.log(chalk.cyan(`↬ ${test.fullTitle()}`)); + if (typeof test.skipReason === 'string') { + console.log(chalk.cyan(`${' '.repeat(test.titlePath().length + 1)}↬ ${test.skipReason}`)); + } + test.skipped = true; + } +} + +module.exports = MongoDBMochaReporter; + +function replaceIllegalXMLCharacters(string) { + // prettier-ignore + return String(string) + .split('"').join('"') + .split('<').join('﹤') + .split('>').join('﹥') + .split('&').join('﹠'); +} + +const ANSI_ESCAPE_REGEX = + // eslint-disable-next-line no-control-regex + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; +function outputToXML(output) { + function cdata(str) { + return `') + .join('\\]\\]\\>')}]]>`; + } + + function makeTag(name, attributes, selfClose, content) { + const attributesString = Object.entries(attributes || {}) + .map(([k, v]) => `${k}="${replaceIllegalXMLCharacters(v)}"`) + .join(' '); + let tag = `<${name}${attributesString ? ' ' + attributesString : ''}`; + if (selfClose) return tag + '/>\n'; + else tag += '>'; + if (content) return tag + content + ``; + return tag; + } + + let s = + '\n\n\n'; + + for (const suite of output.testSuites) { + s += makeTag('testsuite', { + package: suite.package, + id: suite.id, + name: suite.name, + timestamp: suite.timestamp, + hostname: suite.hostname, + tests: suite.tests, + failures: suite.failures, + errors: suite.errors, + time: suite.time + }); + s += '\n\t' + makeTag('properties') + '\n'; // can put metadata here? + for (const test of suite.testCases) { + s += + '\t' + + makeTag( + 'testcase', + { + name: test.name, + classname: test.className, + time: test.time, + start: test.startTime, + end: test.endTime + }, + !test.failure && !test.skipped + ); + if (test.failure) { + s += + '\n\t\t' + + makeTag('failure', { type: test.failure.type }, false, cdata(test.failure.stack)) + + '\n'; + s += `\t\n`; + } + if (test.skipped) { + s += makeTag('skipped', {}, true); + s += `\t\n`; + } + } + s += '\t' + makeTag('system-out', {}, false, cdata(suite.stdout)) + '\n'; + s += '\t' + makeTag('system-err', {}, false, cdata(suite.stderr)) + '\n'; + s += `\n`; + } + + return s + '\n'; +}