From 1d6693fd5abedcc1de2c16fb0f86a2a5ab1ba7c1 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 27 Sep 2020 17:52:48 +0200 Subject: [PATCH] Add implementation --- .c8rc.json | 15 ++++++ .editorconfig | 12 +++++ .github/CODE_OF_CONDUCT.md | 76 +++++++++++++++++++++++++++++++ .github/CONTRIBUTING.md | 7 +++ .github/ISSUE_TEMPLATE/bug.md | 15 ++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++++ .github/ISSUE_TEMPLATE/feature.md | 11 +++++ .github/workflows/ci.yml | 29 ++++++++++++ .gitignore | 3 ++ .npmrc | 1 + README.md | 3 ++ ava.config.js | 11 +++++ package.json | 44 ++++++++++++++++++ source/index.ts | 50 ++++++++++++++++++++ test-d/types.ts | 4 ++ test/test.ts | 37 +++++++++++++++ tsconfig.json | 16 +++++++ xo.config.js | 14 ++++++ 18 files changed, 356 insertions(+) create mode 100644 .c8rc.json create mode 100644 .editorconfig create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 ava.config.js create mode 100644 package.json create mode 100644 source/index.ts create mode 100644 test-d/types.ts create mode 100644 test/test.ts create mode 100644 tsconfig.json create mode 100644 xo.config.js diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 0000000..52ae9a2 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,15 @@ +{ + "all": true, + "include": [ + "{dist/source,source}/**/*.{js,ts}" + ], + "exclude": [ + "{coverage,dist/test}/**", + "dist/**/*.d.ts", + "*.config.js" + ], + "reporter": [ + "html", + "lcov" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1c6314a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2c877ff --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +Translations: [Español](https://github.com/avajs/ava-docs/blob/master/es_ES/code-of-conduct.md), [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/code-of-conduct.md), [Italiano](https://github.com/avajs/ava-docs/blob/master/it_IT/code-of-conduct.md), [日本語](https://github.com/avajs/ava-docs/blob/master/ja_JP/code-of-conduct.md), [Português](https://github.com/avajs/ava-docs/blob/master/pt_BR/code-of-conduct.md), [Русский](https://github.com/avajs/ava-docs/blob/master/ru_RU/code-of-conduct.md), [简体中文](https://github.com/avajs/ava-docs/blob/master/zh_CN/code-of-conduct.md) + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at sindresorhus@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..fc6a725 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing to AVA + +✨ Thanks for contributing to AVA! ✨ + +Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. + +This repository is a part of AVA. Start with reading AVA's [contributing guide](https://github.com/avajs/ava/blob/master/.github/CONTRIBUTING.md). Issue labels may be a little different in this repository but otherwise the same applies. diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..db7320b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,15 @@ +--- +name: Bug Report +about: If something isn't working the way you expect it to +labels: needs triage +--- + +Please provide details about: + +* What you're trying to do +* What happened +* What you expected to happen + +Please share relevant sample code. Or better yet, provide a link to a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + +We'll also need your AVA configuration (in `package.json` or `ava.config.*` configuration files) and how you're invoking AVA. Share the installed AVA version (get it by running `npx ava --version`). diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6f91954 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: AVA on Spectrum + url: https://spectrum.chat/ava + about: Ask questions and discuss in our Spectrum community + - name: Stack Overflow + url: https://stackoverflow.com/questions/tagged/ava + about: Tag your question on Stack Overflow diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..d5d800b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,11 @@ +--- +name: Feature Request +about: Suggestions for new or different behavior +labels: question +--- + +Please provide details about: + +* What you're trying to do +* Why you can't use this plugin for this +* And maybe how you think AVA could handle this diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..56dcc1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Install and test @ava/cooperate +on: + push: + branches: + - master + pull_request: + paths-ignore: + - '*.md' +jobs: + nodejs: + name: Node.js + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [^12.17.0, ^14.0.0] + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install --no-audit + - run: npm test + - uses: codecov/codecov-action@v1 + with: + file: coverage/lcov.info + name: ${{ matrix.node-version }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18f2b36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +coverage +dist +node_modules diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/README.md b/README.md new file mode 100644 index 0000000..9749d21 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# @ava/get-port + +Experimental AVA plugin which works like [`get-port`](https://github.com/sindresorhus/get-port), but ensures the port is locked across all test files. diff --git a/ava.config.js b/ava.config.js new file mode 100644 index 0000000..87fb3d3 --- /dev/null +++ b/ava.config.js @@ -0,0 +1,11 @@ +export default { // eslint-disable-line import/no-anonymous-default-export + files: ['!dist/**'], + nonSemVerExperiments: { + sharedWorkers: true + }, + typescript: { + rewritePaths: { + 'test/': 'dist/test/' + } + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ca68259 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "@ava/get-port", + "version": "0.0.0", + "description": "Cooperative get-port implementation", + "engines": { + "node": ">=12.17.0 <13 || >=14.0.0" + }, + "files": [ + "dist/source" + ], + "main": "dist/source", + "types": "dist/source/index.d.ts", + "scripts": { + "build": "del-cli dist && tsc", + "prepare": "npm run -s build", + "test": "xo && tsd && c8 ava" + }, + "keywords": [ + "ava", + "lock", + "mutex", + "plugin", + "test" + ], + "author": "Mark Wubben (https://novemberborn.net)", + "repository": "avajs/get-port", + "license": "MIT", + "devDependencies": { + "@ava/typescript": "^1.1.1", + "@sindresorhus/tsconfig": "^0.7.0", + "ava": "^3.13.0", + "c8": "^7.3.1", + "del-cli": "^3.0.1", + "tsd": "^0.13.1", + "typescript": "^4.0.3", + "xo": "^0.33.1" + }, + "dependencies": { + "@ava/cooperate": "^0.1.0" + }, + "peerDependencies": { + "ava": "*" + } +} diff --git a/source/index.ts b/source/index.ts new file mode 100644 index 0000000..089c99a --- /dev/null +++ b/source/index.ts @@ -0,0 +1,50 @@ +import crypto from 'crypto'; +import net from 'net'; +import {SharedContext} from '@ava/cooperate'; + +const context = new SharedContext(__filename); + +// Reserve a range of 16 addresses at a random offset. +const reserveRange = async (): Promise => { + let from: number; + do { + from = crypto.randomBytes(2).readUInt16BE(0); + } while (from < 1024 || from > 65520); + + const range = Array.from({length: 16}, (_, index) => from + index); + return context.reserve(...range); +}; + +// Listen on the port to make sure it's available. +const confirmAvailable = async (port: number, options?: net.ListenOptions): Promise => new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on('error', (error: Error & { code: string }) => { + if (error.code === 'EADDRINUSE' || error.code === 'EACCESS') { + resolve(false); + } else { + reject(error); + } + }); + server.listen({...options, port}, () => { + server.close(() => resolve(true)); + }); +}); + +let available: Promise = reserveRange(); +export default async function getPort(options?: Omit): Promise { // eslint-disable-line @typescript-eslint/ban-types + const promise = available; + const range = await promise; + const port = range.shift(); + + if (available === promise && range.length === 0) { + // (Pro-actively) reserve a new range + available = reserveRange(); + } + + if (port === undefined || !(await confirmAvailable(port, options))) { + return getPort(options); + } + + return port; +} diff --git a/test-d/types.ts b/test-d/types.ts new file mode 100644 index 0000000..4b1be5d --- /dev/null +++ b/test-d/types.ts @@ -0,0 +1,4 @@ +import {expectError} from 'tsd'; +import getPort from '..'; + +expectError(await getPort({port: 1024})); diff --git a/test/test.ts b/test/test.ts new file mode 100644 index 0000000..2879867 --- /dev/null +++ b/test/test.ts @@ -0,0 +1,37 @@ +import net from 'net'; +import {promisify} from 'util'; +import test from 'ava'; +import getPort from '../source'; + +test('gets up to 16 ports in a block', async t => { + const first = await getPort(); + let count = 1; + + let newBlock; + while (newBlock === undefined) { + const port = await getPort(); // eslint-disable-line no-await-in-loop + if (port > first && port - 16 <= first) { + count++; + } else { + newBlock = port; + } + } + + t.true(count > 1); + t.true(count <= 16); + + t.log({count, first, newBlock}); +}); + +test('port can be bound', async t => { + const server = net.createServer(); + t.teardown(() => server.close()); + + const port = await getPort(); + await promisify(server.listen.bind(server))(port); + t.is((server.address() as any).port, port); +}); + +test('can get ports simultaneously', async t => { + await t.notThrowsAsync(Promise.all([getPort(), getPort()])); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1b58587 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@sindresorhus/tsconfig", + "compilerOptions": { + "outDir": "dist", + "target": "es2019", // Node.js 12 + "lib": [ + "es2019" + ], + "esModuleInterop": true, + "sourceMap": true + }, + "include": [ + "source", + "test" + ] +} diff --git a/xo.config.js b/xo.config.js new file mode 100644 index 0000000..245a02f --- /dev/null +++ b/xo.config.js @@ -0,0 +1,14 @@ +module.exports = { + rules: { + '@typescript-eslint/prefer-readonly-parameter-types': 'off', + 'no-void': 'off' + }, + overrides: [ + { + files: '**/*.ts', + rules: { + '@typescript-eslint/no-floating-promises': ['error', {ignoreVoid: true}] + } + } + ] +};