Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@actualwave/traceability-matrices

Package Overview
Dependencies
Maintainers
1
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@actualwave/traceability-matrices - npm Package Compare versions

Comparing version 0.0.4 to 0.0.5

cert.pem

48

cli.js

@@ -22,6 +22,2 @@ #!/usr/bin/env node

if (!parsed) {
// FIXME for testing purposes, remove before publishing
console.error("Oh, come on, useless pile of garbage. Git gud, bitch.");
process.exit(1);
break;

@@ -77,5 +73,6 @@ }

/**
* serve --target-dir= --port=
* serve --target-dir= --port= --https=true
*/
const port = args.port ? parseInt(String(args.port), 10) : DEFAULT_PORT;
const useHttps = String(args.https) === "true";

@@ -86,9 +83,11 @@ if (Number.isNaN(port)) {

const { serve } = require("./serve.js");
const { serve } = require("./commands/serve.js");
serve(targetDirs, port);
import("open").then(({ default: open }) =>
open(`http://localhost:${port}`)
);
serve(targetDirs, port, useHttps).then(() => {
import("open").then(({ default: open }) =>
open(
useHttps ? `https://localhost:${port}` : `http://localhost:${port}`
)
);
});
}

@@ -110,8 +109,8 @@ break;

fs.rmSync(outputDir, { recursive: true, force: true });
} else {
exitWithError(`"${outputDir}" exists.`);
}
} else {
fs.mkdirSync(outputDir);
}
const { generateStatic } = require("./generate-static.js");
const { generateStatic } = require("./commands/generate-static.js");

@@ -121,2 +120,23 @@ generateStatic(targetDirs, outputDir);

break;
case "threshold":
{
// threshold --total=80 --per-project=40
const total =
args["total"] === undefined ? 100 : parseInt(args["total"], 10);
const perProject =
args["per-project"] === undefined
? 100
: parseInt(args["per-project"], 10);
if (isNaN(total) || isNaN(perProject)) {
exitWithError(
"Coverage thresholds should be positive integer values between 0 and 100."
);
}
const { threshold } = require("./commands/threshold.js");
threshold(targetDirs, total, perProject);
}
break;
case "help":

@@ -123,0 +143,0 @@ // TODO

@@ -1,8 +0,36 @@

export declare const createProject: (projectTitle: string) => {
structure: (data?: Record<string, object>, headers?: string[]) => void;
export type Project = {
title: String;
description: string;
structure: Record<string, object>;
headers: string[];
records: Record<string, object>;
};
export type ProjectApi = {
structure: (
data?: Record<string, object>,
headers?: string[]
) => {
add: (...path: string[]) => Record<string, object>;
get: (...path: string[]) => Record<string, object>;
merge: (source: Record<string, object>) => void;
clone: (...path: string[]) => Record<string, object>;
branch: (
path: string[],
projectTitle: string,
projectDescription?: string
) => ProjectApi;
narrow: (
path: string[],
projectTitle: string,
projectDescription?: string
) => ProjectApi;
};
headers: (headers?: string[]) => {
clone: () => string[];
get: (index: number) => string;
set: (index: number, title: string) => void;
};
trace: (requirement: string | string[], chainFn?: () => void) => void;
requirement: (requirement: string | string[]) => {
requirement: (...requirement: string[]) => {
describe: (title: string, ...args: any[]) => any;

@@ -16,2 +44,23 @@ context: (title: string, ...args: any[]) => any;

};
valueOf: () => Project;
};
export declare const createProject: (
projectTitle: string,
projectDescription?: string
) => ProjectApi;
export declare const getStructureBranch: (
structure: Record<string, object>,
path: string[]
) => Record<string, object>;
export declare const mergeStructure: (
source: Record<string, object>,
target: Record<string, object>
) => void;
export declare const cloneStructure: (
source: Record<string, object>,
target?: Record<string, object>
) => Record<string, object>;

@@ -35,2 +35,20 @@ const projects = [];

const getStructureBranch = (structure, path) => {
let index = 0;
let parent = structure;
while (index < path.length) {
const name = path[index];
if (!parent[name]) {
return null;
}
parent = parent[name];
index++;
}
return parent;
};
const mergeStructure = (source, target) => {

@@ -46,5 +64,18 @@ Object.entries(source).forEach(([title, children]) => {

export const createProject = (projectTitle) => {
const cloneStructure = (source, target = {}) => {
for (let name in source) {
if (!target[name]) {
target[name] = {};
}
cloneStructure(source[name], target[name]);
}
return target;
};
const createProject = (projectTitle, projectDescription = "") => {
const project = {
title: projectTitle,
description: projectDescription,
structure: {},

@@ -57,2 +88,10 @@ headers: [],

const clone = (projectTitle, projectDescription) => ({
title: projectTitle,
description: projectDescription,
structure: cloneStructure(project.structure),
headers: [],
records: {},
});
const structure = (data, columnHeaders) => {

@@ -66,2 +105,63 @@ if (data) {

}
function cloneProjectStructure(...path) {
const branch = getStructureBranch(project.structure, path);
if (!branch) {
throw new Error(
`Structure path [${
path.length ? `"${path.join('", "')}"` : ""
}] is not available in "${project.title}"`
);
}
return cloneStructure(branch);
}
return {
add: (...path) => {
let index = 0;
let parent = project.structure;
while (index < path.length) {
const name = path[index];
if (!parent[name]) {
parent[name] = {};
}
parent = parent[name];
index++;
}
return parent;
},
get: (...path) => getStructureBranch(project.structure, path),
merge: (struct) => mergeStructure(struct, project.structure),
clone: cloneProjectStructure,
branch: (path, projectTitle, projectDescription) => {
const subProject = createProject(projectTitle, projectDescription);
subProject.structure(cloneProjectStructure(path));
return subProject;
},
narrow: (path, projectTitle, projectDescription) => {
const subProject = createProject(projectTitle, projectDescription);
subProject.valueOf().headers = project.headers.concat();
const sourceStruct = getStructureBranch(project.structure, path);
if (!sourceStruct) {
throw new Error(
`Structure path [${
path.length ? `"${path.join('", "')}"` : ""
}] is not available in "${project.title}"`
);
}
cloneStructure(sourceStruct, subProject.structure().add(path));
return subProject;
},
};
};

@@ -75,2 +175,4 @@

return {
clone: () => project.headers.concat(),
get: (index) => project.headers[index],
set: (index, header) => {

@@ -138,2 +240,3 @@ project.headers[index] = header;

return {
valueOf: () => project,
structure,

@@ -143,2 +246,3 @@ headers,

requirement,
clone,
};

@@ -153,1 +257,6 @@ };

});
module.exports.getStructureBranch = getStructureBranch;
module.exports.mergeStructure = mergeStructure;
module.exports.cloneStructure = cloneStructure;
module.exports.createProject = createProject;
{
"name": "@actualwave/traceability-matrices",
"version": "0.0.4",
"version": "0.0.5",
"types": "cypress.d.ts",

@@ -39,3 +39,11 @@ "bin": {

"pug": "^3.0.2"
},
"scripts": {
"certs": "mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1",
"test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
"jest": "^29.6.3"
}
}

@@ -18,3 +18,7 @@ const { readFile } = require("fs/promises");

const addEmptyRecordsFromStructure = (structure, records, requirements = []) => {
const addEmptyRecordsFromStructure = (
structure,
records,
requirements = []
) => {
Object.entries(structure).forEach(([name, children]) => {

@@ -83,10 +87,14 @@ if (isPopulated(children)) {

const getStructureDepth = (source, depth = 1) => {
Object.values(source).forEach((value) => {
if (isPopulatedStructure(value)) {
depth = Math.max(depth, getStructureDepth(value, depth + 1));
const getStructureDepth = (source, depth = 0) => {
let newDepth = depth;
for (key in source) {
const value = source[key];
if (value && typeof value === "object") {
newDepth = Math.max(newDepth, getStructureDepth(value, depth + 1));
}
});
}
return depth;
return newDepth;
};

@@ -104,2 +112,3 @@

if (global) {
global.description = global.description || source.description;
mergeStructure(source.structure, global.structure);

@@ -109,2 +118,3 @@ } else {

title: source.title,
description: source.description,
structure: source.structure,

@@ -111,0 +121,0 @@ records: {},

# @actualwave/traceability-matrices
# Work in progress
Integrate requirements into e2e/integration test code and generate traceability matrices for your project. Currently it has an adapter to work with [Cypress](https://www.cypress.io/) tests.
Integrate requirements into e2e/integration test code and generate traceability matrices for your project. Currently it has an adapter to work with Cypress.
![One file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_a.png?raw=true)
![Multi-file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_c.png?raw=true)
![One file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_a.png?raw=true)
## How it works
Work with this project starts with placing traces of requirements within a test file.
![Multi-file project](https://github.com/burdiuz/traceability-matrices/blob/master/screenshots/project_c.png?raw=true)
```js
it("should do something according to requirement #1", () => {
project.trace("requirement #1");
expect(something).toEqual(somethingElse);
});
```
Once test is finished, coverage report will be stored in a coverage folder specified in cypress config or environment variable. Stored file is a JSON file and is not suitable for viewung, to generate viewable information and actual matrices/tables user should use command `traceability-matrices generate` to generate static HTML files with reports or `traceability-matrices serve` to start local HTTP server with reports.
## Installation
NPM
First add the package via NPM
```
npm install -D @actualwave/traceability-matrices
```
Yarn
or Yarn
```

@@ -24,10 +37,81 @@ yarn add -D @actualwave/traceability-matrices

## Usage
Add a script to your package.json
Then configure by defining a environment variable `TRACE_RECORDS_DATA_DIR`, this could be done in cypress config file
```js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
env: {
// will store coverage reports in <project-root>/cypress/coverage
TRACE_RECORDS_DATA_DIR: "cypress/coverage",
},
},
});
```
traceability-matrices serve --target-dir=<folder with coverage reports>
Also, it might be useful to add commands to package.json
```json
"scripts": {
"tm:serve": "traceability-matrices serve --target-dir=cypress/coverage",
"tm:generate": "traceability-matrices generate --target-dir=cypress/coverage --output-dir=coverage-static"
},
```
## Commands
This package supports multiple commands to work with generated coverage reports after test run. All commands accept required parameter `--target-dir` which points at coverage reports root folder, it is the same folder defined in `TRACE_RECORDS_DATA_DIR` environment variable. This parameter could be provided multiple times to point at multiple coverage directories.
### traceability-matrices serve
Run HTTP/S server with coverage reports and open in default browser.
Parameters:
- `--target-dir` - required, path to directory with coverage reports.
- `--port` - port for HTTP/S server, 8477 by default
- `--https` - set to "true"(`--https=true`) to start HTTPS server, by default starts HTTP server
Example:
```
traceability-matrices serve --target-dir=cypress/coverage --https=true
```
### traceability-matrices generate
Generate static HTML files with coverage reports.
Parameters:
- `--target-dir` - required, path to directory with coverage reports.
- `--output-dir` - required, path to folder where generated HTML files should be stored
Example:
```
traceability-matrices generate --target-dir=cypress/coverage --output-dir=coverage-static
```
### traceability-matrices threshold
Fails(exits with an error code) if coverage thresholds weren't met.
Parameters:
- `--target-dir` - required, path to directory with coverage reports.
- `--total` - optional, defines global coverage threshold, value can be between 0 and 100. Fails command if combined coverage of all project does not meet threshold.
- `--per-project` - optional, defines coverage threshold applies to every project, value can be between 0 and 100. Fails command if at least one project does not meet threshold.
Example:
```
traceability-matrices threshold --target-dir=cypress/coverage --total=80 --per-project=60
```
## Cypress integration
Add `TRACE_RECORDS_DATA_DIR` environment variable to cypress config that will tell where to store coverage reports
Start with adding `TRACE_RECORDS_DATA_DIR` environment variable to cypress config that will tell where to store coverage reports
```js

@@ -45,35 +129,159 @@ const { defineConfig } = require("cypress");

and defining a project in a test file
This project has a part that integrates into Cypress test files and adds traces of covered requirements. To start import it
```js
import { createProject } from "@actualwave/traceability-matrices/cypress";
const project = createProject("My Project");
```
Then use imported function to create a project
This project provides methods to match test specs with project requriements and genrates test records that will be stored in a JSON file after test run is finished.
### project.trace()
To match requirements with specs engineer should place traces within specs, like
```js
const Project = createProject("My Project");
// test spec
it("should do something according to requirement #1", () => {
// trace requirement
project.trace("requirement #1");
expect(something).toEqual(somethingElse);
});
```
And create traces for requirements within the test specs
After running this test, coverage will contain a record that spec `should do something according to requirement #1` tests `requirement #1` requirement. One spec may contain multiple requriements and traces could contain expectations or be nested.
```js
it("should display entry point path", () => {
Project.trace("Path to App.js should be visible");
cy.get(".App-header p > code").should("contain", "src/App.js");
it("should do something according to multiple requirements", () => {
project.trace("requirement #1", () => {
expect(something).toEqual(somethingElse);
project.trace("requirement #3", () => {
expect(something).toEqual(somethingElse);
project.trace("requirement #4", () => {
expect(something).toEqual(somethingElse);
});
});
});
project.trace("requirement #2");
expect(something).toEqual(somethingElse);
});
```
When test run is finished, coverage report for it will be stored in specified folder. Coverage is generated into a JSON file, to have a human-readable format, run `serve` command.
`project.trace()` could be used to generate requirements tree by providing an array of strings
Using only traces will generate flat requirements structure, if you want to add categories, priorities or groups to it, sefine a structure to the project.
```js
import { createProject } from "@actualwave/traceability-matrices/cypress";
it("should do something according to requirement #1", () => {
project.trace(["High", "General", "PRD I", "requirement #1"]);
project.trace(["High", "General", "PRD I", "requirement #2"]);
project.trace(["High", "General", "PRD I", "requirement #3"]);
project.trace(["Low", "PRD IVa", "requirement #45"]);
project.trace(["Low", "PRD IVa", "requirement #46"]);
project.trace(["optional requirement #1"]);
const Project = createProject("My Project");
expect(something).toEqual(somethingElse);
});
```
Project.structure({
Statics: {
"Add header text": {},
"Path to App.js should be visible": {},
"Add welcome text": {},
}
This will generate a structure of requirements
- High
- General
- PRD I
- requirement #1
- requirement #2
- requirement #3
- Low
- PRD IVa
- requirement #45
- requirement #46
- optional requirement #1
Such structure also could be created using `project.structure()` method
```js
project.structure({
High: {
General: {
"PRD I": {
"requirement #1": {},
"requirement #2": {},
"requirement #3": {},
},
},
},
Low: {
"PRD IVa": {
"requirement #45": {},
"requirement #46": {},
},
},
"optional requirement #1": {},
});
```
When using a trace, it will be matched to a leaf node of the structure with same name.
and with this just use requirement name in `project.trace()` call,
```js
it("should do something according to requirement #1", () => {
project.trace("requirement #3");
expect(something).toEqual(somethingElse);
});
```
it will be matched to leaf node of structure. If requirement not found in structure, it will be added to the root of structure when coverage is generated.
### project.requirement()
With `project.trace()`, engineer could use `project.requirement()` and specify requirement to use in multiple places.
```js
const req1 = project.requirement("requirement #1");
```
Once created, it could be used to replace test lifecycle hooks like `describe()` and `it()`
```js
req1.describe('When someting', () => {
...
});
req1.it('should do', () => {
...
});
```
Both will record requirement being tested in these places.
### project.structure()
`project.structure()` used to specify category tree, ther are no specification but structure should be built only with objects. Leaf objects of this structure will be identified as testable requirementes, other branches are categories and could not be tested.
```js
project.structure({
High: {
General: {
"PRD I": {
"requirement #1": {},
"requirement #2": {},
"requirement #3": {},
},
},
},
});
```
If test will contain a trace to category, that record will be added as a leaf node to the root of the structure.
`project.structure()` return an object with additional methods to work with structure.
- `add(...path: string[]) => Record<string, object>` - add categories/requirements to the structure if not exist
- `get(...path: string[]) => Record<string, object>` - retrieve a branch of the structure
- `merge(source: Record<string, object>) => void` - merge external structure into
- `clone(...path: string[]) => Record<string, object>` - clone and return whole or branch of the structure
- `branch(path: string[], projectTitle: string, projectDescription?: string) => ProjectApi` - create a sub-project from structure branch. sub-project will have no connection to current project and structures will be copied.
- `narrow(path: string[], projectTitle: string, projectDescription?: string) => ProjectApi` - create sub-project with a structure by removing branches out of provided path. sub-project will have no connection to current project and structures will be copied.

@@ -7,5 +7,5 @@ const { renderProject } = require("./project");

*/
const renderFile = (file, state) => {
const renderFile = (file, state, links) => {
const list = Object.values(file.projects).map((source) => {
return renderProject(source, state);
return renderProject(source, state, links);
});

@@ -12,0 +12,0 @@

@@ -15,3 +15,3 @@ const { compile } = require("pug");

div.file
a(href=\`/file?id=\${file.id}\`) #{file.specName}
a(href=self.links.getFileLink(file.id)) #{file.specName}
div.file-projects

@@ -29,5 +29,6 @@ each project in self.listFileProjects(file)

*/
const renderFiles = (state) => {
const renderFiles = (state, links) => {
return fileStructureTemplate({
...state,
links,

@@ -34,0 +35,0 @@ // TODO CACHE totals per file and project

@@ -5,26 +5,40 @@ const { basename } = require("path");

const projectTableTemplate = compile(`
const projectTableTemplate = compile(
`
table.project
if self.projectDescription
tr
td.project-description(colspan=self.requirementsDepth + self.totalSpecCount + 1) !{self.projectDescription}
//- Header Rows
tr.file-headers
th.project-title(colspan=requirementsDepth, rowspan='2') #{projectTitle}
th.project-title(colspan=self.requirementsDepth, rowspan=self.projectHeaders.length ? 1 : 2) #{self.projectTitle} (#{self.coveredRequirements} / #{self.totalRequirements})
th.spec-count(rowspan='2') Spec count
each file in fileHeaders
each file in self.fileHeaders
th.file-name(colspan=file.colspan, title=file.title) #{file.name}
tr.specs-headers
each spec in specHeaders
if self.projectHeaders.length
- var index = 0;
while index < self.requirementsDepth
th.header(title=self.projectHeaders[index]) #{self.projectHeaders[index]}
- index++;
each spec in self.specHeaders
th.spec-name(title=spec.title) #{spec.name}
//- Data Rows
each row, index in dataRows
each row, index in self.dataRows
tr(class=\`result \${row.class}\`)
each header in dataHeaderRows[index]
th(colspan=header.colspan, rowspan=header.rowspan, class=header.class, title=header.title) #{header.name}
each header in self.dataHeaderRows[index]
th(colspan=header.colspan, rowspan=header.rowspan, class=header.class, title=header.title) !{header.name}
each data in row.cells
td(title=data.title, class=data.class) #{data.name}
//- Totals
tr.totals
td(colspan=requirementsDepth) Project Coverage
td(colspan=totalSpecCount+1) #{coveredRequirements} / #{totalRequirements}
`);
td(colspan=self.requirementsDepth) Project Coverage
td(colspan=self.totalSpecCount + 1) #{self.coveredRequirements} / #{self.totalRequirements}
`,
{ self: true }
);

@@ -116,3 +130,5 @@ /**

name: requirement.title,
title: requirement.title,
// if title contains HTML -- strip tags
title: requirement.title.replace(/<\/?[^>]+?>/gi, ""),
colspan: 1,

@@ -205,3 +221,3 @@ rowspan: 1,

*/
const renderProject = (project, state) => {
const renderProject = (project, state, links) => {
/*

@@ -229,2 +245,4 @@ * build headers

projectTitle: project.title,
projectDescription: project.description,
projectHeaders: project.headers || [],
fileHeaders: horizontal.filesRow,

@@ -237,2 +255,3 @@ specHeaders: horizontal.specsRow,

dataHeaderRows: vertical.rows,
links,
});

@@ -239,0 +258,0 @@

@@ -10,3 +10,3 @@ const { compile } = require("pug");

span.totals #{project.requirementsCovered} / #{project.requirementsTotal}
a.title(href=\`/project?id=\${encodeURIComponent(project.title)}\`) #{project.title}
a.title(href=self.links.getProjectLink(project.title)) #{project.title}
`,

@@ -20,3 +20,3 @@ { self: true }

*/
const renderProjects = (state) => {
const renderProjects = (state, links) => {
const list = Object.values(state.projects).map((project) => {

@@ -31,5 +31,5 @@ const stats = calculateProjectStats(project);

return projectsStructureTemplate({ list });
return projectsStructureTemplate({ list, links });
};
module.exports.renderProjects = renderProjects;
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc