-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #450 from balena-io/improve-testing
Improve testing of contracts
- Loading branch information
Showing
14 changed files
with
221 additions
and
103 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
npx --no lint-staged |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"*.ts": [ | ||
"balena-lint --fix" | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package-lock=false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const fs = require('fs'); | ||
|
||
module.exports = JSON.parse( | ||
fs.readFileSync( | ||
__dirname + '/node_modules/@balena/lint/config/.prettierrc', | ||
'utf8', | ||
), | ||
); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,10 +8,6 @@ | |
{ | ||
"type": "arch.sw", | ||
"slug": "armv7hf" | ||
}, | ||
{ | ||
"type": "sw.os-image", | ||
"slug": "resinos" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,17 +8,27 @@ | |
}, | ||
"private": true, | ||
"scripts": { | ||
"test": "eslint scripts && node scripts/check-contracts.js" | ||
"test": "npm run lint && npm run test:node", | ||
"lint": "balena-lint tests", | ||
"lint-fix": "balena-lint --fix tests", | ||
"test:node": "mocha -r ts-node/register --reporter spec tests/**/*.spec.ts --timeout 30000" | ||
}, | ||
"author": "Balena Inc. <[email protected]>", | ||
"license": "Apache-2.0", | ||
"devDependencies": { | ||
"eslint": "^4.8.0", | ||
"eslint-config-standard": "^10.2.1", | ||
"eslint-plugin-import": "^2.7.0", | ||
"eslint-plugin-node": "^5.2.0", | ||
"eslint-plugin-promise": "^3.5.0", | ||
"eslint-plugin-standard": "^3.0.1" | ||
"@balena/contrato": "^0.12.0", | ||
"@balena/lint": "^9.1.3", | ||
"@types/chai": "^4.3.20", | ||
"@types/chai-as-promised": "^7.1.4", | ||
"@types/mocha": "^10.0.10", | ||
"chai": "^4.3.4", | ||
"chai-as-promised": "^7.1.1", | ||
"husky": "^9.1.7", | ||
"lint-staged": "^15.2.11", | ||
"mocha": "^11.0.1", | ||
"p-map": "^7.0.3", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.7.2" | ||
}, | ||
"versionist": { | ||
"publishedAt": "2025-01-28T17:12:51.985Z" | ||
|
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import chai from 'chai'; | ||
import chaiAsPromised from 'chai-as-promised'; | ||
|
||
chai.use(chaiAsPromised); | ||
|
||
export default chai; | ||
|
||
export const { expect } = chai; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { expect } from './chai'; | ||
import * as path from 'path'; | ||
import { promises as fs } from 'fs'; | ||
import type { ContractObject } from '@balena/contrato'; | ||
import { Contract, Universe, query } from '@balena/contrato'; | ||
|
||
const CONTRACTS_PATH = path.join(__dirname, '..', 'contracts'); | ||
|
||
async function findFiles( | ||
dir: string, | ||
filter: (filePath: string) => boolean = () => true, | ||
): Promise<string[]> { | ||
const allFiles = await fs.readdir(dir, { recursive: true }); | ||
const filePaths: string[] = []; | ||
for (const fileName of allFiles) { | ||
const filePath = path.join(dir, fileName); | ||
const stat = await fs.stat(filePath); | ||
if (!stat.isDirectory() && filter(filePath)) { | ||
filePaths.push(filePath); | ||
} | ||
} | ||
|
||
return filePaths; | ||
} | ||
|
||
type ContractMeta = { | ||
type: string; | ||
source: ContractObject; | ||
path: string; | ||
}; | ||
|
||
async function readContracts(dir: string): Promise<ContractMeta[]> { | ||
const allFiles = await findFiles( | ||
dir, | ||
(fileName) => path.extname(fileName) === '.json', | ||
); | ||
|
||
const { default: pMap } = await import('p-map'); | ||
|
||
return ( | ||
await pMap( | ||
allFiles.values(), | ||
async (file) => { | ||
const contents = await fs.readFile(file, { encoding: 'utf8' }); | ||
const source = JSON.parse(contents); | ||
return { | ||
type: path.basename(path.dirname(path.dirname(file))), | ||
source, | ||
path: file, | ||
}; | ||
}, | ||
|
||
{ concurrency: 10 }, | ||
) | ||
).flat(); | ||
} | ||
|
||
describe('Balena Base Contracts', function () { | ||
let allContractsMeta: ContractMeta[]; | ||
|
||
before(async () => { | ||
allContractsMeta = await readContracts(CONTRACTS_PATH); | ||
}); | ||
|
||
it('contracts are stored in the right folder', function () { | ||
for (const contractMeta of allContractsMeta) { | ||
expect( | ||
contractMeta.source.type, | ||
`the contract type '${contractMeta.source.type}' does not match its parent folder '${contractMeta.type}'`, | ||
).to.equal(contractMeta.type); | ||
} | ||
}); | ||
|
||
it('all children in the contract universe are satisfied', function () { | ||
const allContracts = allContractsMeta.flatMap(({ source }) => | ||
Contract.build(source), | ||
); | ||
|
||
const universe = new Universe(); | ||
universe.addChildren(allContracts); | ||
|
||
// The contracts universe is internally consistent | ||
// if all the children requirements are satisfied | ||
expect(universe.getAllNotSatisfiedChildRequirements()).to.deep.equal([]); | ||
}); | ||
|
||
it('os/arch/device contract combinations are satisfied', function () { | ||
const allContracts = allContractsMeta.flatMap(({ source }) => | ||
Contract.build(source), | ||
); | ||
|
||
const universe = new Universe(); | ||
universe.addChildren(allContracts); | ||
|
||
const contexts = query( | ||
universe, | ||
{ | ||
'sw.os': { | ||
cardinality: 1, | ||
filter: { | ||
type: 'object', | ||
properties: { slug: { not: { enum: ['balena-os', 'resinos'] } } }, | ||
}, | ||
}, | ||
'arch.sw': 1, | ||
'sw.blob': '1+', | ||
'sw.stack-variant': 1, | ||
'hw.device-type': 1, | ||
}, | ||
{ type: 'meta.context' }, | ||
); | ||
|
||
for (const context of contexts) { | ||
const unmet = context.getAllNotSatisfiedChildRequirements(); | ||
expect( | ||
unmet, | ||
'Unsatisfied requirements for context: ' + | ||
JSON.stringify(context, null, 2), | ||
).to.deep.equal([]); | ||
} | ||
}); | ||
|
||
// Skipped as it takes too much time to calculate all combinations | ||
// TODO: unskip this once we can improve the efficiency | ||
// of contrato | ||
it.skip('os/arch/device/stack contract combinations are satisfied', function () { | ||
const allContracts = allContractsMeta.flatMap(({ source }) => | ||
Contract.build(source), | ||
); | ||
|
||
const universe = new Universe(); | ||
universe.addChildren(allContracts); | ||
|
||
const contexts = query( | ||
universe, | ||
{ | ||
'sw.os': { | ||
cardinality: 1, | ||
filter: { | ||
type: 'object', | ||
properties: { slug: { not: { enum: ['balena-os', 'resinos'] } } }, | ||
}, | ||
}, | ||
'arch.sw': 1, | ||
'sw.blob': '1+', | ||
'sw.stack': 1, | ||
'sw.stack-variant': 1, | ||
'hw.device-type': 1, | ||
}, | ||
{ type: 'meta.context' }, | ||
); | ||
|
||
for (const context of contexts) { | ||
const unmet = context.getAllNotSatisfiedChildRequirements(); | ||
expect( | ||
unmet, | ||
'Unsatisfied requirements for context: ' + | ||
JSON.stringify(context, null, 2), | ||
).to.deep.equal([]); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "Node16", | ||
"moduleResolution": "node16", | ||
"outDir": "build", | ||
"noUnusedParameters": true, | ||
"noUnusedLocals": true, | ||
"sourceMap": true, | ||
"strict": true, | ||
"target": "es2022", | ||
"declaration": true, | ||
"skipLibCheck": true | ||
}, | ||
"include": [ | ||
"lib/**/*.ts", | ||
"tests/**/*.ts" | ||
] | ||
} |