Skip to content

Commit

Permalink
Merge pull request #116 from Green-Software-Foundation/new-version
Browse files Browse the repository at this point in the history
Releae v0.7.0 Documentation
  • Loading branch information
jmcook1186 authored Oct 5, 2024
2 parents 9295d26 + 13a3d68 commit ee0e666
Show file tree
Hide file tree
Showing 23 changed files with 1,641 additions and 665 deletions.
249 changes: 131 additions & 118 deletions docs/developers/how-to-build-plugins.md

Large diffs are not rendered by default.

98 changes: 56 additions & 42 deletions docs/developers/how-to-refine-plugins.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
sidebar-position: 2
---

# How to make plugins production ready

Our [How to build plugins](./how-to-build-plugins.md) guide covered the basics for how to construct an Impact Framework plugin. This guide will help you to refine your plugin to make it production-ready. These are best practice guidelines - if you intend to contribute your plugin to one of our repositories, following these guidelines will help your PR to get merged. Even if you are not aiming to have a plugin merged into one of our repositories, consistency with our norms is useful for debugging and maintaining and for making your plugin as useful as possible for other Impact Framework developers.
Expand Down Expand Up @@ -34,27 +35,27 @@ We prefer the following ordering of imports in your plugin code:
1. Node built-in modules (e.g. `import fs from 'fs';`)
2. External modules (e.g. `import {z} from 'zod';`)
3. Internal modules (e.g. `import config from 'src/config';`)
4. Interfaces (e.g. `import type {PluginInterface} from 'interfaces'`)
5. Types (e.g. `import {PluginParams} from '../../types/common'`;)
4. Interfaces (e.g. `import {PluginInterface} from '@grnsft/if-core/types';`)
5. Types (e.g. `import {PluginParams} from '@grnsft/if-core/types';`)

### Comments

Each logical unit in the code should be preceded by an appropriate explanatory comment. Sometimes it is useful to include short comments inside a function that clarifies the purpose of a particular statement. Here's an example from our codebase:

```ts
/**
* Calculates the energy consumption for a single input.
*/
const calculateEnergy = (input: PluginParams) => {
const {
'memory/capacity': totalMemory,
'memory/utilization': memoryUtil,
'energy-per-gb': energyPerGB,
} = input;

// GB * kWh/GB == kWh
return totalMemory * (memoryUtil / 100) * energyPerGB;
};
/**
* Calculates the energy consumption for a single input.
*/
const calculateEnergy = (input: PluginParams) => {
const {
'memory/capacity': totalMemory,
'memory/utilization': memoryUtil,
'energy-per-gb': energyPerGB,
} = input;

// GB * kWh/GB == kWh
return totalMemory * (memoryUtil / 100) * energyPerGB;
};
```

### Error handling
Expand All @@ -71,61 +72,74 @@ import {ERRORS} from '@grnsft/if-core/util';

const {MissingInputDataError} = ERRORS;

...
...

throw new MissingInputDataError("my-plugin is missing my-parameter from inputs[0]");
```


### Validation

We recommend using validation techniques to ensure the integrity of input data. Validate input parameters against expected types, ranges, or constraints to prevent runtime errors and ensure data consistency.
We recommend using `inputValidation` property from `PluginFactory` for validation to ensure the integrity of input data. Validate input parameters against expected types, ranges, or constraints to prevent runtime errors and ensure data consistency.

We use `zod` to validate data. Here's an example from our codebase:
You need to use `zod` schema or `InputValidatorFunction`. Here's an example from our codebase:

- When using function with `InputValidatorFunction` type.

```ts
/**
* Checks for required fields in input.
*/
const validateInput = (input: PluginParams) => {
const schema = z
.object({
'cpu/name': z.string(),
})
.refine(allDefined, {
message: '`cpu/name` should be present.',
});
// `inputValidation` from plugin definition
inputValidation: (input: PluginParams, config: ConfigParams) => {
const inputData = {
'input-parameter': input[config['input-parameter']],
};
const validationSchema = z.record(z.string(), z.number());
validate(validationSchema, inputData);

return validate<z.infer<typeof schema>>(schema, input);
}
return input;
};
```

- When using `zod` schema

```ts
// `inputValidation` from plugin definition
inputValidation: z.object({
duration: z.number().gt(0),
vCPUs: z.number().gt(0).default(1),
memory: z.number().gt(0).default(16),
ssd: z.number().gte(0).default(0),
hdd: z.number().gte(0).default(0),
gpu: z.number().gte(0).default(0),
'usage-ratio': z.number().gt(0).default(1),
time: z.number().gt(0).optional(),
});
```

### Code Modularity

Break down complex functionality into smaller, manageable methods with well-defined responsibilities.
Encapsulate related functionality into private methods to promote code reusability and maintainability.


## 3. Unit tests

Your plugin should have unit tests with 100% coverage. We use `jest` to handle unit testing. We strive to have one `describe` per function. Each possible outcome from each function is separated using `it` with a precise and descriptive message.

Here's an example that covers plugin initialization and the happy path for the `execute()` function.

```ts
import {Sum} from '../../../../lib';
import { ERRORS } from '@grnsft/if-core/utils';

import {ERRORS} from '@grnsft/if-core/util/';
import { Sum } from '../../../if-run/builtins/sum';

const {InputValidationError} = ERRORS;
const { InputValidationError, WrongArithmeticExpressionError } = ERRORS;

describe('lib/sum: ', () => {
describe('builtins/sum: ', () => {
describe('Sum: ', () => {
const globalConfig = {
const config = {
'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'],
'output-parameter': 'energy',
};
const sum = Sum(globalConfig);
const parametersMetadata = {};
const sum = Sum(config, parametersMetadata, {});

describe('init: ', () => {
it('successfully initalized.', () => {
Expand Down Expand Up @@ -161,9 +175,9 @@ describe('lib/sum: ', () => {

expect(result).toStrictEqual(expectedResult);
});
}
})
})
});
});
});
```

We have a [dedicated page](./how-to-write-unit-tests.md) explaining in more detail how to write great unit tests for Impact Framework plugins.
Expand Down
161 changes: 77 additions & 84 deletions docs/developers/how-to-write-unit-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,74 @@
sidebar-position: 3
---

# How to write unit tests
# How to write unit tests

Impact Framework unit tests follow a standard format. We use the `jest` testing library. You can run all our existing tests by opening the project directory and running `npm test`. This page explains how you can add new unit tests for your plugins (or add some for our plugins if you notice a gap).

## Test files

Both the IF and the project repositories include a `__test__` directory. Inside, you will find subdirectory `unit/lib` containing directories for each plugin. Your plugin repository should also follow this structure. Inside the plugin directory you can add `index.test.ts`. This is where you write your unit tests. For example, here's the directory tree for our `teads-curve` test file:

The IF includes a `__test__` directory. Inside, you will find subdirectory `if-run/builtins` containing test files for each plugin. Your plugin repository should also follow this structure. Inside the `builtins` you can add `plugin.test.ts`. This is where you write your unit tests. For example, here's the directory tree for our `sum` test file:

```sh

if-unofficial-plugins
if
|
|- src
|
|-__tests__
|
|-unit
|-if-run
|
|-lib
|-builtins
|
teads-curve
|
|- index.test.ts
sum.test.ts
```

## Setting up your test file
You will need to import your plugin so that it can be instantiated and tested. You will also need some elements from `jest/globals`:
For example, these are the imports for our `Sum` plugin.
```ts
import {Sum} from '../../../../lib';
import {ERRORS} from '../../../../util/errors';
const {InputValidationError} = ERRORS;
import { ERRORS } from '@grnsft/if-core/utils';

import { Sum } from '../../../if-run/builtins/sum';

const { InputValidationError } = ERRORS;
```
You may require other imports for your specific set of tests.
## Describe
Each method should have its own dedicated `describe` block.
Each method should have its own dedicated `describe` block.
Your unit tests should have *at least* two `describe` blocks, one to test the plugin initialization and one for `execute`.
Your unit tests should have _at least_ two `describe` blocks, one to test the plugin initialization and one for `execute`.
```ts
describe("init", ()=> {})
describe("execute", ()=> {})
describe('init', () => {});
describe('execute', () => {});
```
For example, here is a describe block checking that the `Sum` plugin initializes correctly:
```typescript
describe('lib/sum: ', () => {
describe('builtins/sum: ', () => {
describe('Sum: ', () => {
const globalConfig = {
const config = {
'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'],
'output-parameter': 'energy',
};
const sum = Sum(globalConfig);
const sum = Sum(config);

describe('init: ', () => {
it('successfully initalized.', () => {
expect(sum).toHaveProperty('metadata');
expect(sum).toHaveProperty('execute');
});
});
})
})
});
});
```
## It
Expand All @@ -81,57 +79,53 @@ Within each `describe` block, each effect to be tested should have a dedicated `
Here's an example of a new `describe` block for the `execute()` method on the `Sum` plugin. The `describe` block indicates that we are testing effects of the `execute()` method. `it` is specific to a single outcome - in this case there are two `it` blocks that test that the plugin returns a specific result in the happy path and throws an exception if the user has provided invalid config data, specifically that the user-provided `cpu/energy` parameter is missing:
```typescript
describe('execute(): ', () => {
it('successfully applies Sum strategy to given input.', async () => {
expect.assertions(1);

const expectedResult = [
{
duration: 3600,
'cpu/energy': 1,
'network/energy': 1,
'memory/energy': 1,
energy: 3,
timestamp: '2021-01-01T00:00:00Z',
},
];

const result = await sum.execute([
{
duration: 3600,
'cpu/energy': 1,
'network/energy': 1,
'memory/energy': 1,
timestamp: '2021-01-01T00:00:00Z',
},
]);

expect(result).toStrictEqual(expectedResult);
});

it('throws an error on missing params in input.', async () => {
const expectedMessage =
'Sum: cpu/energy is missing from the input array.';

expect.assertions(1);

try {
await sum.execute([
{
duration: 3600,
timestamp: '2021-01-01T00:00:00Z',
},
]);
} catch (error) {
expect(error).toStrictEqual(
new InputValidationError(expectedMessage)
);
}
});
})
describe('execute(): ', () => {
it('successfully applies Sum strategy to given input.', async () => {
expect.assertions(1);
const expectedResult = [
{
duration: 3600,
'cpu/energy': 1,
'network/energy': 1,
'memory/energy': 1,
energy: 3,
timestamp: '2021-01-01T00:00:00Z',
},
];
const result = await sum.execute([
{
duration: 3600,
'cpu/energy': 1,
'network/energy': 1,
'memory/energy': 1,
timestamp: '2021-01-01T00:00:00Z',
},
]);
expect(result).toStrictEqual(expectedResult);
});
it('throws an error on missing params in input.', async () => {
const expectedMessage = 'Sum: cpu/energy is missing from the input array.';
expect.assertions(1);
try {
await sum.execute([
{
duration: 3600,
timestamp: '2021-01-01T00:00:00Z',
},
]);
} catch (error) {
expect(error).toStrictEqual(new InputValidationError(expectedMessage));
}
});
});
```

## Errors
We prefer to use `expect` to check the errors returned from a test. We do this by writing `expect` in a `catch` block. Here's an example from our `sci` plugin tests:
Expand All @@ -157,7 +151,7 @@ it('throws an exception on missing functional unit data.', async () => {
});
```
It is also necessary to include `expect.assertions(n)` for testing asynchronous code, where `n` is the number of assertiosn that should be tested before the test completes.
It is also necessary to include `expect.assertions(n)` for testing asynchronous code, where `n` is the number of assertiosn that should be tested before the test completes.
## Mocks
Expand All @@ -170,16 +164,15 @@ We do have mock backends in several of our tests, and we also have a mock data g
Please use `jest --coverage` to see a coverage report for your plugin - your unit tests should yield 100% coverage. The snippet below shows what to expect from the coverage report:
```sh
-------------------------------|---------|----------|---------|---------|-------------------
| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
| --------------------------- | ------- | -------- | ------- | ------- | ----------------- |
| All files | 100 | 100 | 100 | 100 |
| lib | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
| lib/cloud-metadata | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
| lib/e-mem | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
| lib/e-net | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
------------------------------------|---------|----------|---------|---------|-------------------
| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
| --------------------------------- | ------- | -------- | ------- | ------- | ----------------- |
| All files | 100| 100 | 100 | 100 |
| if-run/builtins/coefficient | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
| if-run/builtins/copy-param | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
| if-run/builtins/csv-lookup | 100 | 100 | 100 | 100 |
| index.ts | 100 | 100 | 100 | 100 |
| if-run/builtins/divide | 100 | 94.11 | 100 | 100 |
```
Loading

0 comments on commit ee0e666

Please sign in to comment.