From da0ac9c2218eb0efef79f6455ba671b44b24c9dc Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 28 Jun 2021 14:47:23 +0100 Subject: [PATCH] chore: add modules for interface-blockstore and interface-datastore tests --- .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/open_an_issue.md | 19 + .github/config.yml | 68 ++ .github/dependabot.yml | 31 + .github/workflows/main.yml | 95 +++ .gitignore | 1 + package.json | 5 +- .../interface-blockstore-tests/CHANGELOG.md | 0 .../interface-blockstore-tests/LICENSE-APACHE | 5 + .../interface-blockstore-tests/LICENSE-MIT | 19 + packages/interface-blockstore-tests/README.md | 37 ++ .../interface-blockstore-tests/package.json | 44 ++ .../interface-blockstore-tests/src/index.js | 621 ++++++++++++++++++ .../interface-blockstore-tests/tsconfig.json | 10 + packages/interface-blockstore/package.json | 6 +- .../interface-datastore-tests/CHANGELOG.md | 0 .../interface-datastore-tests/LICENSE-APACHE | 5 + .../interface-datastore-tests/LICENSE-MIT | 19 + packages/interface-datastore-tests/README.md | 37 ++ .../interface-datastore-tests/package.json | 43 ++ .../interface-datastore-tests/src/index.js | 573 ++++++++++++++++ .../interface-datastore-tests/tsconfig.json | 10 + packages/interface-store/package.json | 6 +- 23 files changed, 1654 insertions(+), 8 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/open_an_issue.md create mode 100644 .github/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 packages/interface-blockstore-tests/CHANGELOG.md create mode 100644 packages/interface-blockstore-tests/LICENSE-APACHE create mode 100644 packages/interface-blockstore-tests/LICENSE-MIT create mode 100644 packages/interface-blockstore-tests/README.md create mode 100644 packages/interface-blockstore-tests/package.json create mode 100644 packages/interface-blockstore-tests/src/index.js create mode 100644 packages/interface-blockstore-tests/tsconfig.json create mode 100644 packages/interface-datastore-tests/CHANGELOG.md create mode 100644 packages/interface-datastore-tests/LICENSE-APACHE create mode 100644 packages/interface-datastore-tests/LICENSE-MIT create mode 100644 packages/interface-datastore-tests/README.md create mode 100644 packages/interface-datastore-tests/package.json create mode 100644 packages/interface-datastore-tests/src/index.js create mode 100644 packages/interface-datastore-tests/tsconfig.json diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4b86d719 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Help on IPFS + url: https://ipfs.io/help + about: All information about how and where to get help on IPFS. + - name: IPFS Official Forum + url: https://discuss.ipfs.io + about: Please post general questions, support requests, and discussions here. diff --git a/.github/ISSUE_TEMPLATE/open_an_issue.md b/.github/ISSUE_TEMPLATE/open_an_issue.md new file mode 100644 index 00000000..4fcbd00a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/open_an_issue.md @@ -0,0 +1,19 @@ +--- +name: Open an issue +about: Only for actionable issues relevant to this repository. +title: '' +labels: need/triage +assignees: '' + +--- + diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..ed26646a --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,68 @@ +# Configuration for welcome - https://github.com/behaviorbot/welcome + +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + Thank you for submitting your first issue to this repository! A maintainer + will be here shortly to triage and review. + + In the meantime, please double-check that you have provided all the + necessary information to make this process easy! Any information that can + help save additional round trips is useful! We currently aim to give + initial feedback within **two business days**. If this does not happen, feel + free to leave a comment. + + Please keep an eye on how this issue will be labeled, as labels give an + overview of priorities, assignments and additional actions requested by the + maintainers: + + - "Priority" labels will show how urgent this is for the team. + - "Status" labels will show if this is ready to be worked on, blocked, or in progress. + - "Need" labels will indicate if additional input or analysis is required. + + Finally, remember to use https://discuss.ipfs.io if you just need general + support. + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Thank you for submitting this PR! + + A maintainer will be here shortly to review it. + + We are super grateful, but we are also overloaded! Help us by making sure + that: + + * The context for this PR is clear, with relevant discussion, decisions + and stakeholders linked/mentioned. + + * Your contribution itself is clear (code comments, self-review for the + rest) and in its best form. Follow the [code contribution + guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) + if they apply. + + Getting other community members to do a review would be great help too on + complex PRs (you can ask in the chats/forums). If you are unsure about + something, just leave us a comment. + + Next steps: + + * A maintainer will triage and assign priority to this PR, commenting on + any missing things and potentially assigning a reviewer for high + priority items. + + * The PR gets reviews, discussed and approvals as needed. + + * The PR is merged by maintainers when it has been approved and comments addressed. + + We currently aim to provide initial feedback/triaging within **two business + days**. Please keep an eye on any labelling actions, as these will indicate + priorities and status of your contribution. + + We are very grateful for your contribution! + + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge +# Comment to be posted to on pull requests merged by a first time user +# Currently disabled +#firstPRMergeComment: "" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..cdb3a640 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: + - directory: "/packages/interface-blockstore" + package-ecosystem: npm + schedule: + interval: daily + time: "11:00" + + - directory: "/packages/interface-blockstore-tests" + package-ecosystem: npm + schedule: + interval: daily + time: "11:00" + + - directory: "/packages/interface-datastore" + package-ecosystem: npm + schedule: + interval: daily + time: "11:00" + + - directory: "/packages/interface-datastore-tests" + package-ecosystem: npm + schedule: + interval: daily + time: "11:00" + + - directory: "/packages/interface-store" + package-ecosystem: npm + schedule: + interval: daily + time: "11:00" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a077bee8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,95 @@ +name: ci +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + check: + runs-on: ubuntu-latest + strategy: + matrix: + project: + - packages/interface-blockstore + - packages/interface-blockstore-tests + - packages/interface-datastore + - packages/interface-datastore-tests + - packages/interface-store + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install dependencies + run: npm install + - name: Build types + run: npm run build + - name: Typecheck ${{ matrix.project }} + uses: gozala/typescript-error-reporter-action@v1.0.8 + with: + project: ${{ matrix.project }} + - run: npx aegir build + - run: npx aegir dep-check + - uses: ipfs/aegir/actions/bundle-size@master + name: size + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + project: ${{ matrix.project }} + test-node: + strategy: + matrix: + node-version: [14.x, 16.x] + os: [windows-latest, ubuntu-latest, macos-latest] + project: + - packages/interface-blockstore + - packages/interface-blockstore-tests + - packages/interface-datastore + - packages/interface-datastore-tests + - packages/interface-store + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: ${{ matrix.node-version }} + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npx aegir test -t node --cov --bail + - uses: codecov/codecov-action@v1 + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir test -t browser -t webworker --bail + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir test -t browser -t webworker --bail -- --browser firefox + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx xvfb-maybe aegir test -t electron-main --bail + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx xvfb-maybe aegir test -t electron-renderer --bail diff --git a/.gitignore b/.gitignore index 91a3983f..62bf2cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist node_modules package-lock.json +docs diff --git a/package.json b/package.json index a41cdafe..aa4e8215 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "scripts": { "reset": "rimraf ./node_modules ./package-lock.json packages/*/node_modules packages/*/package-lock.json packages/*/dist", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "lerna run test", "build": "lerna run build", "lint": "lerna run lint", "release": "lerna run build && lerna publish" @@ -22,7 +22,8 @@ "packages/*" ], "dependencies": { - "lerna": "^4.0.0" + "lerna": "^4.0.0", + "rimraf": "^3.0.2" }, "engines": { "npm": ">=7.0.0" diff --git a/packages/interface-blockstore-tests/CHANGELOG.md b/packages/interface-blockstore-tests/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/interface-blockstore-tests/LICENSE-APACHE b/packages/interface-blockstore-tests/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/interface-blockstore-tests/LICENSE-APACHE @@ -0,0 +1,5 @@ +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. diff --git a/packages/interface-blockstore-tests/LICENSE-MIT b/packages/interface-blockstore-tests/LICENSE-MIT new file mode 100644 index 00000000..749aa1ec --- /dev/null +++ b/packages/interface-blockstore-tests/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/packages/interface-blockstore-tests/README.md b/packages/interface-blockstore-tests/README.md new file mode 100644 index 00000000..48ad345e --- /dev/null +++ b/packages/interface-blockstore-tests/README.md @@ -0,0 +1,37 @@ +# interface-blockstore-tests + +> A test suite for [interface-blockstore](https://github.com/ipfs/js-ipfs-interfaces/tree/master/packages/interface-blockstore) implementations + +## Table of Contents + +- [Usage](#usage) +- [Contribute](#contribute) +- [License](#license) + +## Usage + +```js +const MyBlockstore = require('./path/to/my-blockstore') +const suite = require('interface-blockstore-tests') + +describe('MyBlockstore', () => { + describe('interface-blockstore compliance tests', () => { + suite({ + setup () { + return new MyBlockstore() + }, + teardown () {} + }) + }) +}) +``` + +## Contribute + +PRs accepted. + +Small note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +## License + +[Apache-2.0](LICENSE-APACHE) OR [MIT](LICENSE-MIT) diff --git a/packages/interface-blockstore-tests/package.json b/packages/interface-blockstore-tests/package.json new file mode 100644 index 00000000..0cccc7ea --- /dev/null +++ b/packages/interface-blockstore-tests/package.json @@ -0,0 +1,44 @@ +{ + "name": "interface-blockstore-tests", + "version": "0.0.0", + "description": "Compliance tests for the blockstore interface", + "leadMaintainer": "Alex Potsides ", + "main": "src/index.js", + "types": "dist/src/index.d.ts", + "files": [ + "src", + "dist" + ], + "scripts": { + "build": "aegir build", + "lint": "aegir ts -p check && aegir lint", + "test": "echo \"No tests configured\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/interface-blockstore.git" + }, + "keywords": [ + "interface", + "key-value", + "ipfs", + "blockstore" + ], + "license": "(Apache-2.0 OR MIT)", + "bugs": { + "url": "https://github.com/ipfs/interface-blockstore/issues" + }, + "homepage": "https://github.com/ipfs/js-ipfs-interfaces/tree/master/packages/interface-blockstore-tests#readme", + "dependencies": { + "aegir": "^33.1.2", + "interface-blockstore": "^0.1.0", + "it-all": "^1.0.2", + "it-drain": "^1.0.1", + "it-length": "^1.0.2", + "multiformats": "^9.1.0", + "uint8arrays": "^2.1.5" + }, + "eslintConfig": { + "extends": "ipfs" + } +} diff --git a/packages/interface-blockstore-tests/src/index.js b/packages/interface-blockstore-tests/src/index.js new file mode 100644 index 00000000..5f809f77 --- /dev/null +++ b/packages/interface-blockstore-tests/src/index.js @@ -0,0 +1,621 @@ +/* eslint-env mocha */ +'use strict' + +const { expect } = require('aegir/utils/chai') +const all = require('it-all') +const drain = require('it-drain') +const uint8ArrayFromString = require('uint8arrays/from-string') +const { CID } = require('multiformats/cid') +const { sha256 } = require('multiformats/hashes/sha2') +const raw = require('multiformats/codecs/raw') +const length = require('it-length') + +/** + * @typedef {import('interface-blockstore').Blockstore} Blockstore + * @typedef {import('interface-blockstore').Pair} Pair + * @typedef {import('interface-blockstore').QueryOrder} QueryOrder + * @typedef {import('interface-blockstore').QueryFilter} QueryFilter + * @typedef {import('interface-blockstore').KeyQueryOrder} KeyQueryOrder + * @typedef {import('interface-blockstore').KeyQueryFilter} KeyQueryFilter + */ + +async function getKeyValuePair () { + const value = uint8ArrayFromString(`data-${Math.random()}`) + const hash = await sha256.digest(value) + const key = CID.createV1(raw.code, hash) + + return { key, value } +} + +/** + * @param {number} count + */ +async function getKeyValuePairs (count) { + return Promise.all( + new Array(count).fill(0).map((_, i) => getKeyValuePair()) + ) +} + +/** + * @param {{ teardown: () => void; setup: () => Blockstore; }} test + */ +module.exports = (test) => { + /** + * @param {Blockstore} store + */ + const cleanup = async store => { + await store.close() + await test.teardown() + } + + const createStore = async () => { + const store = await test.setup() + if (!store) throw new Error('missing store') + await store.open() + return store + } + + describe('put', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const { key, value } = await getKeyValuePair() + + return store.put(key, value) + }) + + it('parallel', async () => { + const data = await getKeyValuePairs(100) + + await Promise.all(data.map(d => store.put(d.key, d.value))) + + const res = await all(store.getMany(data.map(d => d.key))) + expect(res).to.deep.equal(data.map(d => d.value)) + }) + }) + + describe('putMany', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('streaming', async () => { + const data = await getKeyValuePairs(100) + + let index = 0 + + for await (const { key, value } of store.putMany(data)) { + expect(data[index]).to.deep.equal({ key, value }) + index++ + } + + expect(index).to.equal(data.length) + + const res = await all(store.getMany(data.map(d => d.key))) + expect(res).to.deep.equal(data.map(d => d.value)) + }) + }) + + describe('get', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const { + key, value + } = await getKeyValuePair() + + await store.put(key, value) + + const res = await store.get(key) + expect(res).to.equalBytes(value) + }) + + it('should throw error for missing key', async () => { + const { + key + } = await getKeyValuePair() + + try { + await store.get(key) + } catch (err) { + expect(err).to.have.property('code', 'ERR_NOT_FOUND') + return + } + + throw new Error('expected error to be thrown') + }) + }) + + describe('getMany', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('streaming', async () => { + const { + key, value + } = await getKeyValuePair() + + await store.put(key, value) + const source = [key] + + const res = await all(store.getMany(source)) + expect(res).to.have.lengthOf(1) + expect(res[0]).to.equalBytes(value) + }) + + it('should throw error for missing key', async () => { + const { + key + } = await getKeyValuePair() + + try { + await drain(store.getMany([key])) + } catch (err) { + expect(err).to.have.property('code', 'ERR_NOT_FOUND') + return + } + + throw new Error('expected error to be thrown') + }) + }) + + describe('delete', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const { + key, value + } = await getKeyValuePair() + + await store.put(key, value) + await store.get(key) + await store.delete(key) + const exists = await store.has(key) + expect(exists).to.be.eql(false) + }) + + it('parallel', async () => { + const data = await getKeyValuePairs(100) + + await Promise.all(data.map(d => store.put(d.key, d.value))) + + const res0 = await Promise.all(data.map(d => store.has(d.key))) + res0.forEach(res => expect(res).to.be.eql(true)) + + await Promise.all(data.map(d => store.delete(d.key))) + + const res1 = await Promise.all(data.map(d => store.has(d.key))) + res1.forEach(res => expect(res).to.be.eql(false)) + }) + }) + + describe('deleteMany', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('streaming', async () => { + const data = await getKeyValuePairs(100) + + await drain(store.putMany(data)) + + const res0 = await Promise.all(data.map(d => store.has(d.key))) + res0.forEach(res => expect(res).to.be.eql(true)) + + let index = 0 + + for await (const key of store.deleteMany(data.map(d => d.key))) { + expect(data[index].key).to.deep.equal(key) + index++ + } + + expect(index).to.equal(data.length) + + const res1 = await Promise.all(data.map(d => store.has(d.key))) + res1.forEach(res => expect(res).to.be.eql(false)) + }) + }) + + describe('batch', () => { + /** @type {Blockstore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const data = await getKeyValuePairs(4) + + const b = store.batch() + + await store.put(data[0].key, data[0].value) + + b.put(data[1].key, data[1].value) + b.put(data[2].key, data[2].value) + b.put(data[3].key, data[3].value) + b.delete(data[0].key) + await b.commit() + + const keys = data.map(d => d.key) + const res = await Promise.all(keys.map(k => store.has(k))) + + expect(res).to.be.eql([false, true, true, true]) + }) + + it('many (1200)', async function () { + this.timeout(20 * 1000) + const b = store.batch() + const count = 1200 + + /** @type {Record} */ + const prefixes = {} + + for (let i = 0; i < count; i++) { + const { + key, value + } = await getKeyValuePair() + + b.put(key, value) + + const prefix = key.toString().substr(0, 3) + + prefixes[prefix] = (prefixes[prefix] || 0) + 1 + } + + await b.commit() + + await Promise.all( + Object.keys(prefixes) + .map(prefix => { + return expect(length(store.query({ prefix }))).to.eventually.equal(prefixes[prefix]) + }) + ) + }) + }) + + describe('query', () => { + /** @type {Blockstore} */ + let store + /** @type {Pair} */ + let hello + /** @type {Pair} */ + let world + /** @type {Pair} */ + let hello2 + /** @type {QueryFilter} */ + let filter1 + /** @type {QueryFilter} */ + let filter2 + + before(async () => { + hello = await getKeyValuePair() + hello2 = await getKeyValuePair() + world = await getKeyValuePair() + + filter1 = entry => !entry.key.toString().endsWith(hello.key.toString().substring(-5)) + filter2 = entry => entry.key.toString().endsWith(hello2.key.toString().substring(-5)) + }) + + /** + * @type {QueryOrder} + */ + const order1 = (a, b) => { + if (a.value.toString() < b.value.toString()) { + return -1 + } + return 1 + } + + /** + * @type {QueryOrder} + */ + const order2 = (a, b) => { + if (a.value.toString() < b.value.toString()) { + return 1 + } + if (a.value.toString() > b.value.toString()) { + return -1 + } + return 0 + } + + /** @type {Array<{ name: string, test: () => { query: any, expected: any}}>} */ + const tests = [ + { name: 'empty', test: () => ({ query: {}, expected: [hello, world, hello2] }) }, + { name: 'prefix', test: () => ({ query: { prefix: '/z' }, expected: [world, hello2] }) }, + { name: '1 filter', test: () => ({ query: { filters: [filter1] }, expected: [world, hello2] }) }, + { name: '2 filters', test: () => ({ query: { filters: [filter1, filter2] }, expected: [hello2] }) }, + { name: 'limit', test: () => ({ query: { limit: 1 }, expected: 1 }) }, + { name: 'offset', test: () => ({ query: { offset: 1 }, expected: 2 }) }, + { name: '1 order (1)', test: () => ({ query: { orders: [order1] }, expected: [hello, world, hello2] }) }, + { name: '1 order (reverse 1)', test: () => ({ query: { orders: [order2] }, expected: [hello2, world, hello] }) } + ] + + before(async () => { + store = await createStore() + + const b = store.batch() + + b.put(hello.key, hello.value) + b.put(world.key, world.value) + b.put(hello2.key, hello2.value) + + return b.commit() + }) + + after(() => cleanup(store)) + + tests.forEach(({ name, test }) => it(name, async () => { + const { + query, expected + } = test() + let res = await all(store.query(query)) + + if (Array.isArray(expected)) { + if (query.orders == null) { + expect(res).to.have.length(expected.length) + /** + * @param {Pair} a + * @param {Pair} b + */ + const s = (a, b) => { + if (a.key.toString() < b.key.toString()) { + return 1 + } else { + return -1 + } + } + res = res.sort(s) + const exp = expected.sort(s) + + res.forEach((r, i) => { + expect(r.key.toString()).to.be.eql(exp[i].key.toString()) + + if (r.value == null) { + expect(exp[i].value).to.not.exist() + } else { + expect(r.value).to.deep.equal(exp[i].value) + } + }) + } else { + expect(res).to.be.eql(expected) + } + } else if (typeof expected === 'number') { + expect(res).to.have.length(expected) + } + })) + + it('allows mutating the datastore during a query', async () => { + const hello3 = await getKeyValuePair() + let firstIteration = true + + for await (const {} of store.query({})) { // eslint-disable-line no-empty-pattern + if (firstIteration) { + expect(await store.has(hello2.key)).to.be.true() + await store.delete(hello2.key) + expect(await store.has(hello2.key)).to.be.false() + + await store.put(hello3.key, hello3.value) + firstIteration = false + } + } + + const results = await all(store.query({})) + + expect(firstIteration).to.be.false('Query did not return anything') + expect(results.map(result => result.key)).to.have.deep.members([ + hello.key, + world.key, + hello3.key + ]) + }) + + it('queries while the datastore is being mutated', async () => { + const { + key, value + } = await getKeyValuePair() + const writePromise = store.put(key, value) + const results = await all(store.query({})) + expect(results.length).to.be.greaterThan(0) + await writePromise + }) + }) + + describe('queryKeys', () => { + /** @type {Blockstore} */ + let store + /** @type {Pair} */ + let hello + /** @type {Pair} */ + let world + /** @type {Pair} */ + let hello2 + /** @type {KeyQueryFilter} */ + let filter1 + /** @type {KeyQueryFilter} */ + let filter2 + + before(async () => { + hello = await getKeyValuePair() + hello2 = await getKeyValuePair() + world = await getKeyValuePair() + + filter1 = key => !key.toString().endsWith(hello.key.toString().substring(-5)) + filter2 = key => key.toString().endsWith(hello2.key.toString().substring(-5)) + }) + + /** + * @type {KeyQueryOrder} + */ + const order1 = (a, b) => { + if (a.toString() < b.toString()) { + return -1 + } + return 1 + } + + /** + * @type {KeyQueryOrder} + */ + const order2 = (a, b) => { + if (a.toString() < b.toString()) { + return 1 + } + if (a.toString() > b.toString()) { + return -1 + } + return 0 + } + + /** @type {Array<{ name: string, test: () => { query: any, expected: any}}>} */ + const tests = [ + { name: 'empty', test: () => ({ query: {}, expected: [hello.key, world.key, hello2.key] }) }, + { name: 'prefix', test: () => ({ query: { prefix: '/z' }, expected: [world.key, hello2.key] }) }, + { name: '1 filter', test: () => ({ query: { filters: [filter1] }, expected: [world.key, hello2.key] }) }, + { name: '2 filters', test: () => ({ query: { filters: [filter1, filter2] }, expected: [hello2.key] }) }, + { name: 'limit', test: () => ({ query: { limit: 1 }, expected: 1 }) }, + { name: 'offset', test: () => ({ query: { offset: 1 }, expected: 2 }) }, + { name: '1 order (1)', test: () => ({ query: { orders: [order1] }, expected: [hello.key, world.key, hello2.key] }) }, + { name: '1 order (reverse 1)', test: () => ({ query: { orders: [order2] }, expected: [hello2.key, world.key, hello.key] }) } + ] + + before(async () => { + store = await createStore() + + const b = store.batch() + + b.put(hello.key, hello.value) + b.put(world.key, world.value) + b.put(hello2.key, hello2.value) + + return b.commit() + }) + + after(() => cleanup(store)) + + tests.forEach(({ name, test }) => it(name, async () => { + const { + query, expected + } = test() + let res = await all(store.queryKeys(query)) + + if (Array.isArray(expected)) { + if (query.orders == null) { + expect(res).to.have.length(expected.length) + /** + * @type {KeyQueryOrder} + */ + const s = (a, b) => { + if (a.toString() < b.toString()) { + return 1 + } else { + return -1 + } + } + res = res.sort(s) + const exp = expected.sort(s) + + res.forEach((r, i) => { + expect(r.toString()).to.be.eql(exp[i].toString()) + }) + } else { + expect(res).to.be.eql(expected) + } + } else if (typeof expected === 'number') { + expect(res).to.have.length(expected) + } + })) + + it('allows mutating the datastore during a query', async () => { + const hello3 = await getKeyValuePair() + let firstIteration = true + + for await (const {} of store.queryKeys({})) { // eslint-disable-line no-empty-pattern + if (firstIteration) { + expect(await store.has(hello2.key)).to.be.true() + await store.delete(hello2.key) + expect(await store.has(hello2.key)).to.be.false() + + await store.put(hello3.key, hello3.value) + firstIteration = false + } + } + + const results = await all(store.queryKeys({})) + + expect(firstIteration).to.be.false('Query did not return anything') + expect(results).to.have.deep.members([ + hello.key, + world.key, + hello3.key + ]) + }) + + it('queries while the datastore is being mutated', async () => { + const { key, value } = await getKeyValuePair() + const writePromise = store.put(key, value) + const results = await all(store.queryKeys({})) + expect(results.length).to.be.greaterThan(0) + await writePromise + }) + }) + + describe('lifecycle', () => { + /** @type {Blockstore} */ + let store + + before(async () => { + store = await test.setup() + if (!store) throw new Error('missing store') + }) + + after(() => cleanup(store)) + + it('close and open', async () => { + await store.close() + await store.open() + await store.close() + await store.open() + }) + }) +} diff --git a/packages/interface-blockstore-tests/tsconfig.json b/packages/interface-blockstore-tests/tsconfig.json new file mode 100644 index 00000000..07e8db2b --- /dev/null +++ b/packages/interface-blockstore-tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "test", + "src" + ] +} diff --git a/packages/interface-blockstore/package.json b/packages/interface-blockstore/package.json index e58e4133..212dc9e0 100644 --- a/packages/interface-blockstore/package.json +++ b/packages/interface-blockstore/package.json @@ -5,9 +5,9 @@ "main": "src/index.js", "types": "dist/src/index.d.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "aegir ts -p check && aegir lint", - "build": "aegir build" + "build": "aegir build", + "test": "echo \"No tests configured\"", + "lint": "aegir ts -p check && aegir lint" }, "license": "(Apache-2.0 OR MIT)", "homepage": "https://github.com/ipfs/js-ipfs-interfaces/tree/master/packages/interface-blockstore#readme", diff --git a/packages/interface-datastore-tests/CHANGELOG.md b/packages/interface-datastore-tests/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/interface-datastore-tests/LICENSE-APACHE b/packages/interface-datastore-tests/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/interface-datastore-tests/LICENSE-APACHE @@ -0,0 +1,5 @@ +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. diff --git a/packages/interface-datastore-tests/LICENSE-MIT b/packages/interface-datastore-tests/LICENSE-MIT new file mode 100644 index 00000000..749aa1ec --- /dev/null +++ b/packages/interface-datastore-tests/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/packages/interface-datastore-tests/README.md b/packages/interface-datastore-tests/README.md new file mode 100644 index 00000000..1b64bddc --- /dev/null +++ b/packages/interface-datastore-tests/README.md @@ -0,0 +1,37 @@ +# interface-datastore-tests + +> A test suite for [interface-datastore](https://github.com/ipfs/interface-datastore) implementations + +## Table of Contents + +- [Usage](#usage) +- [Contribute](#contribute) +- [License](#license) + +## Usage + +```js +const MyDatastore = require('./path/to/my-datastore') +const suite = require('interface-datastore-tests') + +describe('MyDatastore', () => { + describe('interface-datastore compliance tests', () => { + suite({ + setup () { + return new MyDatastore() + }, + teardown () {} + }) + }) +}) +``` + +## Contribute + +PRs accepted. + +Small note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +## License + +[Apache-2.0](LICENSE-APACHE) OR [MIT](LICENSE-MIT) diff --git a/packages/interface-datastore-tests/package.json b/packages/interface-datastore-tests/package.json new file mode 100644 index 00000000..c75caef6 --- /dev/null +++ b/packages/interface-datastore-tests/package.json @@ -0,0 +1,43 @@ +{ + "name": "interface-datastore-tests", + "version": "0.0.0", + "description": "Compliance tests for the datastore interface", + "leadMaintainer": "Alex Potsides ", + "main": "src/index.js", + "types": "dist/src/index.d.ts", + "files": [ + "src", + "dist" + ], + "scripts": { + "build": "aegir build", + "lint": "aegir ts -p check && aegir lint", + "test": "echo \"No tests configured\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/js-ipfs-interfaces.git" + }, + "keywords": [ + "interface", + "key-value", + "ipfs", + "datastore" + ], + "license": "(Apache-2.0 OR MIT)", + "bugs": { + "url": "https://github.com/ipfs/js-ipfs-interfaces/issues" + }, + "homepage": "https://github.com/ipfs/js-ipfs-interfaces/tree/master/packages/interface-datastore-tests#readme", + "dependencies": { + "aegir": "^33.1.2", + "interface-datastore": "^4.0.2", + "iso-random-stream": "^2.0.0", + "it-all": "^1.0.2", + "it-drain": "^1.0.1", + "uint8arrays": "^2.1.5" + }, + "eslintConfig": { + "extends": "ipfs" + } +} diff --git a/packages/interface-datastore-tests/src/index.js b/packages/interface-datastore-tests/src/index.js new file mode 100644 index 00000000..5564f936 --- /dev/null +++ b/packages/interface-datastore-tests/src/index.js @@ -0,0 +1,573 @@ +/* eslint-env mocha */ +'use strict' + +const { randomBytes } = require('iso-random-stream') +const { expect } = require('aegir/utils/chai') +const all = require('it-all') +const drain = require('it-drain') +const uint8ArrayFromString = require('uint8arrays/from-string') +const { Key } = require('interface-datastore') + +/** + * @typedef {import('interface-datastore').Datastore} Datastore + * @typedef {import('interface-datastore').Pair} Pair + * @typedef {import('interface-datastore').QueryOrder} QueryOrder + * @typedef {import('interface-datastore').QueryFilter} QueryFilter + * @typedef {import('interface-datastore').KeyQueryOrder} KeyQueryOrder + * @typedef {import('interface-datastore').KeyQueryFilter} KeyQueryFilter + */ + +/** + * @param {{ teardown: () => void; setup: () => Datastore; }} test + */ +module.exports = (test) => { + /** + * @param {Datastore} store + */ + const cleanup = async store => { + await store.close() + await test.teardown() + } + + const createStore = async () => { + const store = await test.setup() + if (!store) throw new Error('missing store') + await store.open() + return store + } + + describe('put', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', () => { + const k = new Key('/z/one') + return store.put(k, uint8ArrayFromString('one')) + }) + + it('parallel', async () => { + const data = [] + for (let i = 0; i < 100; i++) { + data.push({ key: new Key(`/z/key${i}`), value: uint8ArrayFromString(`data${i}`) }) + } + + await Promise.all(data.map(d => store.put(d.key, d.value))) + + const res = await all(store.getMany(data.map(d => d.key))) + expect(res).to.deep.equal(data.map(d => d.value)) + }) + }) + + describe('putMany', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('streaming', async () => { + const data = [] + for (let i = 0; i < 100; i++) { + data.push({ key: new Key(`/z/key${i}`), value: uint8ArrayFromString(`data${i}`) }) + } + + let index = 0 + + for await (const { key, value } of store.putMany(data)) { + expect(data[index]).to.deep.equal({ key, value }) + index++ + } + + expect(index).to.equal(data.length) + + const res = await all(store.getMany(data.map(d => d.key))) + expect(res).to.deep.equal(data.map(d => d.value)) + }) + }) + + describe('get', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const k = new Key('/z/one') + await store.put(k, uint8ArrayFromString('hello')) + const res = await store.get(k) + expect(res).to.be.eql(uint8ArrayFromString('hello')) + }) + + it('should throw error for missing key', async () => { + const k = new Key('/does/not/exist') + + try { + await store.get(k) + } catch (err) { + expect(err).to.have.property('code', 'ERR_NOT_FOUND') + return + } + + throw new Error('expected error to be thrown') + }) + }) + + describe('getMany', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('streaming', async () => { + const k = new Key('/z/one') + await store.put(k, uint8ArrayFromString('hello')) + const source = [k] + + const res = await all(store.getMany(source)) + expect(res).to.have.lengthOf(1) + expect(res[0]).to.be.eql(uint8ArrayFromString('hello')) + }) + + it('should throw error for missing key', async () => { + const k = new Key('/does/not/exist') + + try { + await drain(store.getMany([k])) + } catch (err) { + expect(err).to.have.property('code', 'ERR_NOT_FOUND') + return + } + + throw new Error('expected error to be thrown') + }) + }) + + describe('delete', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const k = new Key('/z/one') + await store.put(k, uint8ArrayFromString('hello')) + await store.get(k) + await store.delete(k) + const exists = await store.has(k) + expect(exists).to.be.eql(false) + }) + + it('parallel', async () => { + /** @type {[Key, Uint8Array][]} */ + const data = [] + for (let i = 0; i < 100; i++) { + data.push([new Key(`/a/key${i}`), uint8ArrayFromString(`data${i}`)]) + } + + await Promise.all(data.map(d => store.put(d[0], d[1]))) + + const res0 = await Promise.all(data.map(d => store.has(d[0]))) + res0.forEach(res => expect(res).to.be.eql(true)) + + await Promise.all(data.map(d => store.delete(d[0]))) + + const res1 = await Promise.all(data.map(d => store.has(d[0]))) + res1.forEach(res => expect(res).to.be.eql(false)) + }) + }) + + describe('deleteMany', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('streaming', async () => { + const data = [] + for (let i = 0; i < 100; i++) { + data.push({ key: new Key(`/a/key${i}`), value: uint8ArrayFromString(`data${i}`) }) + } + + await drain(store.putMany(data)) + + const res0 = await Promise.all(data.map(d => store.has(d.key))) + res0.forEach(res => expect(res).to.be.eql(true)) + + let index = 0 + + for await (const key of store.deleteMany(data.map(d => d.key))) { + expect(data[index].key).to.deep.equal(key) + index++ + } + + expect(index).to.equal(data.length) + + const res1 = await Promise.all(data.map(d => store.has(d.key))) + res1.forEach(res => expect(res).to.be.eql(false)) + }) + }) + + describe('batch', () => { + /** @type {Datastore} */ + let store + + beforeEach(async () => { + store = await createStore() + }) + + afterEach(() => cleanup(store)) + + it('simple', async () => { + const b = store.batch() + + await store.put(new Key('/z/old'), uint8ArrayFromString('old')) + + b.put(new Key('/a/one'), uint8ArrayFromString('1')) + b.put(new Key('/q/two'), uint8ArrayFromString('2')) + b.put(new Key('/q/three'), uint8ArrayFromString('3')) + b.delete(new Key('/z/old')) + await b.commit() + + const keys = ['/a/one', '/q/two', '/q/three', '/z/old'] + const res = await Promise.all(keys.map(k => store.has(new Key(k)))) + + expect(res).to.be.eql([true, true, true, false]) + }) + + it('many (3 * 400)', async function () { + this.timeout(20 * 1000) + const b = store.batch() + const count = 400 + for (let i = 0; i < count; i++) { + b.put(new Key(`/a/hello${i}`), randomBytes(32)) + b.put(new Key(`/q/hello${i}`), randomBytes(64)) + b.put(new Key(`/z/hello${i}`), randomBytes(128)) + } + + await b.commit() + + /** + * @param {AsyncIterable} iterable + */ + const total = async iterable => { + let count = 0 + // eslint-disable-next-line no-unused-vars + for await (const _ of iterable) count++ + return count + } + + expect(await total(store.query({ prefix: '/a' }))).to.equal(count) + expect(await total(store.query({ prefix: '/z' }))).to.equal(count) + expect(await total(store.query({ prefix: '/q' }))).to.equal(count) + }) + }) + + describe('query', () => { + /** @type {Datastore} */ + let store + const hello = { key: new Key('/q/1hello'), value: uint8ArrayFromString('1') } + const world = { key: new Key('/z/2world'), value: uint8ArrayFromString('2') } + const hello2 = { key: new Key('/z/3hello2'), value: uint8ArrayFromString('3') } + + /** + * @type {QueryFilter} + */ + const filter1 = entry => !entry.key.toString().endsWith('hello') + + /** + * @type {QueryFilter} + */ + const filter2 = entry => entry.key.toString().endsWith('hello2') + + /** + * @type {QueryOrder} + */ + const order1 = (a, b) => { + if (a.value.toString() < b.value.toString()) { + return -1 + } + return 1 + } + + /** + * @type {QueryOrder} + */ + const order2 = (a, b) => { + if (a.value.toString() < b.value.toString()) { + return 1 + } + if (a.value.toString() > b.value.toString()) { + return -1 + } + return 0 + } + + /** @type {Array<[string, any, any[]|number]>} */ + const tests = [ + ['empty', {}, [hello, world, hello2]], + ['prefix', { prefix: '/z' }, [world, hello2]], + ['1 filter', { filters: [filter1] }, [world, hello2]], + ['2 filters', { filters: [filter1, filter2] }, [hello2]], + ['limit', { limit: 1 }, 1], + ['offset', { offset: 1 }, 2], + ['1 order (1)', { orders: [order1] }, [hello, world, hello2]], + ['1 order (reverse 1)', { orders: [order2] }, [hello2, world, hello]] + ] + + before(async () => { + store = await createStore() + + const b = store.batch() + + b.put(hello.key, hello.value) + b.put(world.key, world.value) + b.put(hello2.key, hello2.value) + + return b.commit() + }) + + after(() => cleanup(store)) + + tests.forEach(([name, query, expected]) => it(name, async () => { + let res = await all(store.query(query)) + + if (Array.isArray(expected)) { + if (query.orders == null) { + expect(res).to.have.length(expected.length) + /** + * @param {Pair} a + * @param {Pair} b + */ + const s = (a, b) => { + if (a.key.toString() < b.key.toString()) { + return 1 + } else { + return -1 + } + } + res = res.sort(s) + const exp = expected.sort(s) + + res.forEach((r, i) => { + expect(r.key.toString()).to.be.eql(exp[i].key.toString()) + + if (r.value == null) { + expect(exp[i].value).to.not.exist() + } else { + expect(r.value).to.deep.equal(exp[i].value) + } + }) + } else { + expect(res).to.be.eql(expected) + } + } else if (typeof expected === 'number') { + expect(res).to.have.length(expected) + } + })) + + it('allows mutating the datastore during a query', async () => { + const hello3 = { key: new Key('/z/4hello3'), value: uint8ArrayFromString('4') } + let firstIteration = true + + for await (const {} of store.query({})) { // eslint-disable-line no-empty-pattern + if (firstIteration) { + expect(await store.has(hello2.key)).to.be.true() + await store.delete(hello2.key) + expect(await store.has(hello2.key)).to.be.false() + + await store.put(hello3.key, hello3.value) + firstIteration = false + } + } + + const results = await all(store.query({})) + + expect(firstIteration).to.be.false('Query did not return anything') + expect(results.map(result => result.key)).to.have.deep.members([ + hello.key, + world.key, + hello3.key + ]) + }) + + it('queries while the datastore is being mutated', async () => { + const writePromise = store.put(new Key(`/z/key-${Math.random()}`), uint8ArrayFromString('0')) + const results = await all(store.query({})) + expect(results.length).to.be.greaterThan(0) + await writePromise + }) + }) + + describe('queryKeys', () => { + /** @type {Datastore} */ + let store + const hello = { key: new Key('/q/1hello'), value: uint8ArrayFromString('1') } + const world = { key: new Key('/z/2world'), value: uint8ArrayFromString('2') } + const hello2 = { key: new Key('/z/3hello2'), value: uint8ArrayFromString('3') } + + /** + * @type {KeyQueryFilter} + */ + const filter1 = key => !key.toString().endsWith('hello') + + /** + * @type {KeyQueryFilter} + */ + const filter2 = key => key.toString().endsWith('hello2') + + /** + * @type {KeyQueryOrder} + */ + const order1 = (a, b) => { + if (a.toString() < b.toString()) { + return -1 + } + return 1 + } + + /** + * @type {KeyQueryOrder} + */ + const order2 = (a, b) => { + if (a.toString() < b.toString()) { + return 1 + } + if (a.toString() > b.toString()) { + return -1 + } + return 0 + } + + /** @type {Array<[string, any, any[]|number]>} */ + const tests = [ + ['empty', {}, [hello.key, world.key, hello2.key]], + ['prefix', { prefix: '/z' }, [world.key, hello2.key]], + ['1 filter', { filters: [filter1] }, [world.key, hello2.key]], + ['2 filters', { filters: [filter1, filter2] }, [hello2.key]], + ['limit', { limit: 1 }, 1], + ['offset', { offset: 1 }, 2], + ['1 order (1)', { orders: [order1] }, [hello.key, world.key, hello2.key]], + ['1 order (reverse 1)', { orders: [order2] }, [hello2.key, world.key, hello.key]] + ] + + before(async () => { + store = await createStore() + + const b = store.batch() + + b.put(hello.key, hello.value) + b.put(world.key, world.value) + b.put(hello2.key, hello2.value) + + return b.commit() + }) + + after(() => cleanup(store)) + + tests.forEach(([name, query, expected]) => it(name, async () => { + let res = await all(store.queryKeys(query)) + + if (Array.isArray(expected)) { + if (query.orders == null) { + expect(res).to.have.length(expected.length) + /** + * @type {KeyQueryOrder} + */ + const s = (a, b) => { + if (a.toString() < b.toString()) { + return 1 + } else { + return -1 + } + } + res = res.sort(s) + const exp = expected.sort(s) + + res.forEach((r, i) => { + expect(r.toString()).to.be.eql(exp[i].toString()) + }) + } else { + expect(res).to.be.eql(expected) + } + } else if (typeof expected === 'number') { + expect(res).to.have.length(expected) + } + })) + + it('allows mutating the datastore during a query', async () => { + const hello3 = { key: new Key('/z/4hello3'), value: uint8ArrayFromString('4') } + let firstIteration = true + + for await (const {} of store.queryKeys({})) { // eslint-disable-line no-empty-pattern + if (firstIteration) { + expect(await store.has(hello2.key)).to.be.true() + await store.delete(hello2.key) + expect(await store.has(hello2.key)).to.be.false() + + await store.put(hello3.key, hello3.value) + firstIteration = false + } + } + + const results = await all(store.queryKeys({})) + + expect(firstIteration).to.be.false('Query did not return anything') + expect(results).to.have.deep.members([ + hello.key, + world.key, + hello3.key + ]) + }) + + it('queries while the datastore is being mutated', async () => { + const writePromise = store.put(new Key(`/z/key-${Math.random()}`), uint8ArrayFromString('0')) + const results = await all(store.queryKeys({})) + expect(results.length).to.be.greaterThan(0) + await writePromise + }) + }) + + describe('lifecycle', () => { + /** @type {Datastore} */ + let store + + before(async () => { + store = await test.setup() + if (!store) throw new Error('missing store') + }) + + after(() => cleanup(store)) + + it('close and open', async () => { + await store.close() + await store.open() + await store.close() + await store.open() + }) + }) +} diff --git a/packages/interface-datastore-tests/tsconfig.json b/packages/interface-datastore-tests/tsconfig.json new file mode 100644 index 00000000..07e8db2b --- /dev/null +++ b/packages/interface-datastore-tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "test", + "src" + ] +} diff --git a/packages/interface-store/package.json b/packages/interface-store/package.json index 8be5bc0f..00bf5300 100644 --- a/packages/interface-store/package.json +++ b/packages/interface-store/package.json @@ -5,9 +5,9 @@ "main": "src/index.js", "types": "dist/src/index.d.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "aegir ts -p check && aegir lint", - "build": "aegir build" + "build": "aegir build", + "test": "echo \"No tests configured\"", + "lint": "aegir ts -p check && aegir lint" }, "homepage": "https://github.com/ipfs/js-ipfs-interfaces/tree/master/packages/interface-store#readme", "bugs": "https://github.com/ipfs/js-ipfs-interfaces/issues",