diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3974becb0..d1ea0db53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,15 +24,15 @@ The following document is a rule set of guidelines for contributing. ## What and when to contribute -You can contribute anything to the IF, but we are likely to close out unsolicited PRs without merging them. Our issue board is completely open and we have tags (`help-wanted`, `good-first-issue`) to help contributors to choose tasks to work on. We recommend speaking to the core team on Github before starting working on an issue. You can do this by raising an issue or commenting on an existing issue. This helps us to direct your energy in directions that are aligned with our roadmap, prevent multiple people working on the same task, and better manage our board. This all makes it much more likely that your work will get merged. +You can contribute anything to the IF, but we are likely to close out unsolicited PRs without merging them. Our issue board is completely open and we have tags (`core-only`, `good-first-issue`) to help contributors to choose tasks to work on. If an issue is unassigned and does not have the `core-only` label, it is available to work on. We recommend speaking to the core team on Github before starting working on an issue. You can do this by commenting on an existing issue or discussion thread or starting a new one if appropriate. This helps us to direct your energy in directions that are aligned with our roadmap, prevent multiple people working on the same task, and better manage our board. This all makes it much more likely that your work will get merged. You can also contribute by participating in discussions on our mailing list at [if-community@greensoftware.foundation](https://groups.google.com/u/1/a/greensoftware.foundation/g/if-community). We send out weekly updates that includes what we've shipped, what we're working on and how you can get involved each week. ## Reporting bugs -We appreciate bug reports! If you experience an issue with IF or one of our plugins, you can report it using our bug reporting template. To do this: +We appreciate bug reports! If you experience an issue with IF, you can report it using our bug reporting template. To do this: -1. Go to the [IF repository](https://github.com/Green-Software-Foundation/if) (or [plugin repository](https://github.com/Green-Software-Foundation/if-plugins) if you bug relates to a specific plugin) +1. Go to the [IF repository](https://github.com/Green-Software-Foundation/if) 2. Click on the `Issues` tab 3. Click on `Create New Issue` and select the `Bug Report` template. 4. Fill out the requested information. @@ -46,8 +46,7 @@ The assessment rubric is as follows: | | Consequence | Severity | | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | | Bugs in IF core leading to incorrect calculations | unusable framework | 5 | -| Bugs in if-plugins leading to incorrect calculations | core pathways fail, IF very limited in functionality | 5 | -| Bugs in if-unofficial-pluginsd leading to incorrect calculations | Third party plugins harder to use, limits IF to standard lib | 3 | +| Bugs in builtins leading to incorrect calculations | core pathways fail, IF very limited in functionality | 5 | | Bugs in template | Harder to build plugins, ecosystem growth is impacted | 2 | | Bugs in docs | product does not match expectation, hard to debug, frustration, loss of adoption | 2 | | Security flaw: privacy related | leak user data, unlikely to achieve adoption in serious orgs | 5 | diff --git a/README.md b/README.md index c34bf0069..6fe0f67a4 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,6 @@ > [!IMPORTANT] > Incubation Project: This project is an incubation project being run inside the Green Software Foundation; as such, we DON’T recommend using it in any critical use case. Incubation projects are experimental, offer no support guarantee, have minimal governance and process, and may be retired at any moment. This project may one day Graduate, in which case this disclaimer will be removed. -**Note** We have recently (March 2024) refactored the IF codebase and introduced some changes affecting both users and developers. You can read our migration guide [HERE](./Refactor-migration-guide.md) to help you update to the latest version! - -[Impact Framework](https://greensoftwarefoundation.atlassian.net/wiki/spaces/~612dd45e45cd76006a84071a/pages/17072136/Opensource+Impact+Engine+Framework) (IF) is an [Incubation](https://oc.greensoftware.foundation/project-lifecycle.html#incubation) project from the [Open Source Working Group](https://greensoftwarefoundation.atlassian.net/wiki/spaces/~612dd45e45cd76006a84071a/pages/852049/Open+Source+Working+Group) in the [Green Software Foundation](https://greensoftware.foundation/). - **Our documentation is online at [if.greensoftware.foundation](https://if.greensoftware.foundation/)** **IF** is a framework to **M**odel, **M**easure, si**M**ulate and **M**onitor the environmental impacts of software @@ -23,39 +19,32 @@ Read the [specification and design docs](https://if.greensoftware.foundation) to ## Get started -The first thing to understand is that IF is a framework for running plugins. This means that in order to do some calculations, you need to load some plugins from some external resource. We provide a [standard library of plugins](https://github.com/Green-Software-Foundation/if-plugins) and a repository of [community plugins](https://github.com/Green-Software-Foundation/if-unofficial-plugins) to get you started. +IF is a framework for running pipelines of plugins that operate on a set of observations. This is all configured using a manifest file. We provide a standard library of plugins that come bundled with IF - we refer to these as `builtins`. We also have an [Explorer](https://explorer.if.greensoftware.foundation) where anyone can list third party plugins you can install. -Start by installing framework itself: +Start by installing the latest version of IF: ```sh npm install -g "@grnsft/if" ``` -Then installing some plugins: - -```sh -npm install -g "@grnsft/if-plugins" -``` - Then create a `manifest` file that describes your application (see our docs for a detailed explanation). Then, run `if` using the following command: ```sh -ie --manifest +if-run --manifest --stdout ## or you can use aliases -ie -m +if-run -m -s + ``` -Note that above command will not print the final output. In order to print the final output to the console, run `if` using the optional stdout argument: +Note that above command will print your outputs to the console. You can also provide the `--output` command to save your outputs to a yaml file: ```sh -ie --manifest --stdout -## or using aliases -ie -m -s +if-run -m -o ``` -You can also add a savepath for your output yaml in order to have the output stored in a file. Note that you also have to add configuration to your manifest to enable this, as follows: +Note that you also have to add configuration to your manifest to enable this, as follows: ```yaml initialize: @@ -63,34 +52,27 @@ initialize: - yaml ``` -On the command line: - -```sh -ie --manifest --output -## or using aliases -ie -m -o -``` - -The `ie` CLI tool will configure and run the plugins defined in your input `yaml` (`manifest`) and return the results as an output `yaml` (`output`). +The `if-run` CLI tool will configure and run the plugins defined in your input `yaml` (`manifest`) and return the results as an output `yaml` (`output`). Use the `debug` command if you want to diagnose and fix errors in your plugin: ```sh -ie --manifest --debug +if-run --manifest --debug ``` Use the `help` command if you need guidance about the available commands ```sh -ie --help +if-run --help ## or using alias -ie -h +if-run -h ``` ## Documentation Please read our documentation at [if.greensoftware.foundation](https://if.greensoftware.foundation/) + ## Video walk-through Watch this video to learn how to create and run a `manifest`. @@ -107,11 +89,10 @@ We have a public mailing list at [if-community@greensoftware.foundation](https:/ To contribute to IF, please fork this repository and raise a pull request from your fork. -You can check our issue board for issues tagged `help-wanted`. These are issues that are not currently, actively being worked on by the core team but are well-scoped enough for someone to pick up. We recommend commenting on the issue to start a chat with the core team, then start working on the issue when you have been assigned to it. This process helps to ensure your work is aligned with our roadmap and makes it much more likely that your changes will get merged compared to unsolicited PRs. +You can check our issue board for issues. We mark some issues `core-only` if they are somehow sensitive and we want one of our core developers to handle it. Any other issues are open for the community to work on. We recommend commenting on the issue to start a chat with the core team, then start working on the issue when you have been assigned to it. This process helps to ensure your work is aligned with our roadmap and makes it much more likely that your changes will get merged compared to unsolicited PRs. Please read the full contribution guidelines at [if.greensoftware.foundation](https://if.greensoftware.foundation/Contributing) -The same guidelines also apply to `if-docs`, `if-plugins` and `if-unofficial-plugins`. ## Bug reports diff --git a/Refactor-migration-guide.md b/Refactor-migration-guide.md index 80328fe9e..d315d5c41 100644 --- a/Refactor-migration-guide.md +++ b/Refactor-migration-guide.md @@ -12,13 +12,13 @@ There have been some name changes to the CLI, specifically: The command line tool has been renamed from `impact-engine` to simply `ie`. This means that to invoke the Impact Framework on the command line you simply use ``` - ie ... + if-run ... ``` - `impl` --> `manifest` We have deprecated the original `impl` and `ompl` terminology across all our repositories and on the command line. Now, to pass a manifest file to IF, you use the `--manifest` command, as follows: ```sh - ie --manifest + if-run --manifest ``` @@ -27,7 +27,7 @@ There have been some name changes to the CLI, specifically: We have deprecated the original `impl` and `ompl` terminology across all our repositories and on the command line. Now, to define a savepath for your output file, you use the `--output` command, as follows: ```sh - ie --manifest --output + if-run --manifest --output ``` ## Outputs @@ -55,13 +55,13 @@ npm i @grnsft/if Then run IF using the following command: ```sh -ie --manifest +if-run --manifest ``` This will dump the output to the console. If you want to save the output to a yaml file, provide a savepath to the `--output` command: ```sh -ie --manifest --output +if-run --manifest --output ``` @@ -186,7 +186,6 @@ There have also been some changes to the structure of manifest files. Some of th device/expected-lifespan: 94608000 # 3 years in seconds resources-reserved: 1 resources-total: 8 - functional-unit-time: "1 min" ``` diff --git a/jest.config.js b/jest.config.js index ed24da708..016be5863 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,8 +12,8 @@ module.exports = { }, modulePathIgnorePatterns: [ './build', - './src/__tests__/unit/lib/manifest', './src/__tests__/integration/helpers', './src/__tests__/integration/test-data', ], + coveragePathIgnorePatterns: ['src/config', 'src/types'], }; diff --git a/manifests/bugs/aggregation-error-wrong-metric.yml b/manifests/bugs/aggregation-error-wrong-metric.yml index e9a9af6ea..2c3141c13 100644 --- a/manifests/bugs/aggregation-error-wrong-metric.yml +++ b/manifests/bugs/aggregation-error-wrong-metric.yml @@ -7,26 +7,62 @@ aggregation: type: "both" initialize: plugins: - "teads-curve": - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - "sci-e": - path: "@grnsft/if-plugins" - method: SciE + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide + path: "builtin" + global-config: + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh "sci-embodied": path: "builtin" method: SciEmbodied - "sci-o": - path: "@grnsft/if-plugins" - method: SciO + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-energy-kwh", "grid/carbon-intensity"] + output-parameter: "carbon" "sci": path: "builtin" method: Sci global-config: functional-unit: "requests" - functional-unit-time: "1 minute" "time-sync": method: TimeSync path: "builtin" @@ -42,10 +78,14 @@ tree: children: child-1: pipeline: - - teads-curve - - sci-e + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - time-sync - sci config: @@ -59,36 +99,43 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 - functional-unit-time: "1 min" + vcpus-allocated: 1 + vcpus-total: 8 inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 cloud/region: uk-west duration: 1 cpu/utilization: 10 + requests: 100 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 20 cloud/instance-type: A1 cloud/region: uk-west + requests: 100 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 15 cloud/instance-type: A1 cloud/region: uk-west + requests: 100 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cloud/instance-type: A1 cloud/region: uk-west cpu/utilization: 15 + requests: 100 child-2: pipeline: - - teads-curve - - sci-e + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - time-sync - sci config: @@ -102,27 +149,30 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 - functional-unit-time: "1 min" + vcpus-allocated: 1 + vcpus-total: 8 inputs: - timestamp: "2023-12-12T00:00:00.000Z" duration: 1 cpu/utilization: 30 cloud/instance-type: A1 cloud/region: uk-west + requests: 100 - timestamp: "2023-12-12T00:00:01.000Z" duration: 5 cpu/utilization: 28 cloud/instance-type: A1 cloud/region: uk-west + requests: 100 - timestamp: "2023-12-12T00:00:06.000Z" duration: 7 cpu/utilization: 40 cloud/instance-type: A1 cloud/region: uk-west + requests: 100 - timestamp: "2023-12-12T00:00:13.000Z" duration: 30 cpu/utilization: 33 cloud/instance-type: A1 cloud/region: uk-west + requests: 100 diff --git a/manifests/bugs/azure-importer-ignoring-defaults.yml b/manifests/bugs/azure-importer-ignoring-defaults.yml index 424be5e84..b6749ee70 100644 --- a/manifests/bugs/azure-importer-ignoring-defaults.yml +++ b/manifests/bugs/azure-importer-ignoring-defaults.yml @@ -28,9 +28,12 @@ initialize: input-parameter: network/energy coefficient: 1000 output-parameter: network/energy - "sci-o": - method: SciO - path: "@grnsft/if-plugins" + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["energy", "grid/carbon-intensity"] + output-parameter: "carbon-operational" "group-by": path: "builtin" method: GroupBy diff --git a/manifests/bugs/azure-importer-incorrect-calculation.yml b/manifests/bugs/azure-importer-incorrect-calculation.yml index fa4cc17d0..bbac70ae2 100644 --- a/manifests/bugs/azure-importer-incorrect-calculation.yml +++ b/manifests/bugs/azure-importer-incorrect-calculation.yml @@ -20,9 +20,12 @@ initialize: input-parameter: network/energy coefficient: 1000 output-parameter: network/energy - sci-o: - path: '@grnsft/if-plugins' - method: SciO + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["energy", "grid/carbon-intensity"] + output-parameter: "carbon-operational" group-by: path: builtin method: GroupBy diff --git a/manifests/bugs/initialize-error-no-config.yml b/manifests/bugs/initialize-error-no-config.yml index 67973b53f..466c7f5f9 100644 --- a/manifests/bugs/initialize-error-no-config.yml +++ b/manifests/bugs/initialize-error-no-config.yml @@ -3,18 +3,17 @@ description: a negative test case that fails due to plugin initialization missin tags: initialize: plugins: - teads-curve: - path: '@grnsft/if-unofficial-plugins' - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - outputs: ['yaml'] tree: children: child-0: defaults: cpu/thermal-design-power: 100 pipeline: - - teads-curve + - interpolate inputs: - timestamp: 2023-07-06T00:00 duration: 1 diff --git a/manifests/bugs/initialize-error-no-path.yml b/manifests/bugs/initialize-error-no-path.yml index 21210c544..279f304e6 100644 --- a/manifests/bugs/initialize-error-no-path.yml +++ b/manifests/bugs/initialize-error-no-path.yml @@ -3,19 +3,23 @@ description: a negative test case that fails because the path is mising in a plu tags: initialize: plugins: - teads-curve: + "interpolate": + method: Interpolation path: - method: TeadsCurve global-config: - interpolation: spline - outputs: ['yaml'] + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + # outputs: ['yaml'] tree: children: child-0: defaults: cpu/thermal-design-power: 100 pipeline: - - teads-curve + - interpolate inputs: - timestamp: 2023-07-06T00:00 duration: 1 diff --git a/manifests/bugs/input-error-missing-duration.yml b/manifests/bugs/input-error-missing-duration.yml index 1a4a5d9ef..a2e7f3ca4 100644 --- a/manifests/bugs/input-error-missing-duration.yml +++ b/manifests/bugs/input-error-missing-duration.yml @@ -3,19 +3,23 @@ description: a negative test case that fails due to the required `duration` fiel tags: initialize: plugins: - teads-curve: - path: '@grnsft/if-unofficial-plugins' - method: TeadsCurve + "interpolate": + method: Interpolation + path: builtin global-config: - interpolation: spline - outputs: ['yaml'] + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + # outputs: ['yaml'] tree: children: child-0: defaults: cpu/thermal-design-power: 100 pipeline: - - teads-curve + - interpolate inputs: - timestamp: 2023-07-06T00:00 cpu/utilization: 20 diff --git a/manifests/bugs/pipeline-error-naming-mismatch.yml b/manifests/bugs/pipeline-error-naming-mismatch.yml index 22bb804a4..982e89088 100644 --- a/manifests/bugs/pipeline-error-naming-mismatch.yml +++ b/manifests/bugs/pipeline-error-naming-mismatch.yml @@ -3,11 +3,15 @@ description: a negative test case that fails due to the plugin name in the pipel tags: initialize: plugins: - teads-curve: - path: '@grnsft/if-unofficial-plugins' - method: TeadsCurve + "interpolate": + method: Interpolation + path: builtin global-config: - interpolation: spline + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' outputs: ['yaml'] tree: children: diff --git a/manifests/bugs/pipeline-error-uninitialized-plugin.yml b/manifests/bugs/pipeline-error-uninitialized-plugin.yml index 95afc196b..7bead556b 100644 --- a/manifests/bugs/pipeline-error-uninitialized-plugin.yml +++ b/manifests/bugs/pipeline-error-uninitialized-plugin.yml @@ -4,18 +4,22 @@ tags: initialize: # outputs: ['yaml'] plugins: - teads-curve: - path: '@grnsft/if-unofficial-plugins' - method: TeadsCurve + "interpolate": + method: Interpolation + path: builtin global-config: - interpolation: spline + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' tree: children: child-0: defaults: cpu/thermal-design-power: 100 pipeline: - - teads-curve + - interpolate - multiply inputs: - timestamp: 2023-07-06T00:00 diff --git a/manifests/bugs/pipeline-ordering-error.yml b/manifests/bugs/pipeline-ordering-error.yml index 3538465ea..7fa8836c1 100644 --- a/manifests/bugs/pipeline-ordering-error.yml +++ b/manifests/bugs/pipeline-ordering-error.yml @@ -3,49 +3,58 @@ description: a negative test case that fails because sci-o is invoked too early tags: initialize: plugins: - "teads-curve": - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - "sum": - path: "builtin" - method: Sum + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin global-config: - input-parameters: - - cpu/energy - - network/energy - output-parameter: energy - "sci-embodied": + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide path: "builtin" - method: SciEmbodied - "sci-o": - path: "@grnsft/if-plugins" - method: SciO - "sci": + global-config: + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide path: "builtin" - method: Sci global-config: - functional-unit: "" - functional-unit-time: "1-day" - "time-sync": - method: TimeSync + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide path: "builtin" global-config: - start-time: "2023-12-12T00:00:00.000Z" - end-time: "2023-12-12T00:01:00.000Z" - interval: 5 - allow-padding: true + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh tree: children: child-1: pipeline: - - teads-curve - - sci-o - - sum - - sci-embodied - - time-sync - - sci + - interpolate + - correct-cpu-energy-for-vcpu-ratio + - calculate-vcpu-ratio + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh config: defaults: cpu/thermal-design-power: 100 @@ -53,8 +62,8 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-allocated: 1 + vcpus-total: 8 inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 diff --git a/manifests/examples/basic.yml b/manifests/examples/basic.yml deleted file mode 100644 index e737f5d5b..000000000 --- a/manifests/examples/basic.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: basic -description: a minimal manifest executing a single plugin on a single component for a single timestep -tags: -initialize: - plugins: - teads-curve: - path: '@grnsft/if-unofficial-plugins' - method: TeadsCurve - global-config: - interpolation: spline - outputs: ['yaml'] -tree: - children: - child-0: - defaults: - cpu/thermal-design-power: 100 - pipeline: - - teads-curve - inputs: - - timestamp: 2023-07-06T00:00 - duration: 1 - cpu/utilization: 20 - - timestamp: 2023-07-06T00:01 - duration: 1 - cpu/utilization: 80 - - timestamp: 2023-07-06T00:02 - duration: 1 - cpu/utilization: 20 diff --git a/manifests/examples/copy.yaml b/manifests/examples/copy.yaml new file mode 100644 index 000000000..f2ee816e4 --- /dev/null +++ b/manifests/examples/copy.yaml @@ -0,0 +1,20 @@ +name: copy-param +description: +tags: +initialize: + plugins: + copy-param: + path: builtin + method: Copy + global-config: + keep-existing: true + from: original + to: copy +tree: + children: + child-1: + pipeline: + - copy-param + inputs: + - timestamp: "2023-12-12T00:00:00.000Z" + original: 'hello' diff --git a/manifests/examples/generics.yml b/manifests/examples/generics.yml index c3ffa52e2..c319967f0 100644 --- a/manifests/examples/generics.yml +++ b/manifests/examples/generics.yml @@ -3,24 +3,53 @@ description: a pipeline that does arbitrary calculations using our generic arith tags: initialize: plugins: - teads-curve: - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - "sum": + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide + path: "builtin" + global-config: + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide path: "builtin" - method: Sum global-config: - input-parameters: - - cpu/energy - - network/energy - output-parameter: energy-sum + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh "coefficient": path: "builtin" method: Coefficient global-config: - input-parameter: energy + input-parameter: cpu-energy-kwh coefficient: 2 output-parameter: energy-doubled "multiply": @@ -33,13 +62,19 @@ tree: children: child-1: pipeline: - - teads-curve - - sum + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - coefficient - multiply config: defaults: cpu/thermal-design-power: 100 + vcpus-allocated: 1 + vcpus-total: 8 inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 diff --git a/manifests/examples/instance-metadata.yml b/manifests/examples/instance-metadata.yml index 4ed443985..ea0998241 100644 --- a/manifests/examples/instance-metadata.yml +++ b/manifests/examples/instance-metadata.yml @@ -1,6 +1,6 @@ name: csv-demo -description: null -tags: null +description: +tags: initialize: plugins: cloud-instance-metadata: @@ -19,8 +19,8 @@ initialize: parameter: cpu-model-name match: /^([^,])+/g output: cpu/name - outputs: - - yaml + # outputs: + # - yaml tree: children: child: diff --git a/manifests/examples/mock-cpu-util-to-carbon.yml b/manifests/examples/mock-cpu-util-to-carbon.yml index 35f7cea26..64e6d6719 100644 --- a/manifests/examples/mock-cpu-util-to-carbon.yml +++ b/manifests/examples/mock-cpu-util-to-carbon.yml @@ -21,8 +21,14 @@ initialize: global-config: interpolation: spline cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-azure-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] mock-observations: path: 'builtin' method: MockObservations diff --git a/manifests/examples/nesting.yml b/manifests/examples/nesting.yml index e56e8bb18..978510f48 100644 --- a/manifests/examples/nesting.yml +++ b/manifests/examples/nesting.yml @@ -7,31 +7,62 @@ tags: aggregation: metrics: - "carbon" - - "energy" type: "both" params: initialize: - outputs: ['yaml'] + # outputs: ['yaml'] plugins: - teads-curve: - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - sum: + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide path: "builtin" - method: Sum global-config: - input-parameters: - - cpu/energy - - network/energy - output-parameter: energy + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh sci-embodied: path: "builtin" method: SciEmbodied - sci-o: - path: "@grnsft/if-plugins" - method: SciO + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-energy-kwh", "grid/carbon-intensity"] + output-parameter: "carbon-operational" sci: path: "builtin" method: Sci @@ -62,13 +93,17 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-allocated: 1 + vcpus-total: 8 pipeline: - - teads-curve - - sum + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -108,13 +143,17 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-allocated: 1 + vcpus-total: 8 pipeline: - - teads-curve - - sum + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -156,13 +195,17 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-allocated: 1 + vcpus-total: 8 pipeline: - - teads-curve - - sum + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -202,13 +245,17 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-allocated: 1 + vcpus-total: 8 pipeline: - - teads-curve - - sum + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci diff --git a/manifests/examples/pipeline-teads-sci.yml b/manifests/examples/pipeline-teads-sci.yml index 01c491cdc..c07b093bd 100644 --- a/manifests/examples/pipeline-teads-sci.yml +++ b/manifests/examples/pipeline-teads-sci.yml @@ -3,25 +3,57 @@ description: a full pipeline seeded with some hardcoded input data and yielding tags: initialize: plugins: - "teads-curve": - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - "sum": + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide path: "builtin" - method: Sum global-config: - input-parameters: - - cpu/energy - - network/energy - output-parameter: energy + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh "sci-embodied": path: "builtin" method: SciEmbodied - "sci-o": - path: "@grnsft/if-plugins" - method: SciO + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-energy-kwh", "grid/carbon-intensity"] + output-parameter: "carbon-operational" "sci": path: "builtin" method: Sci @@ -47,10 +79,14 @@ tree: children: child-1: pipeline: - - teads-curve - - sum + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - sci config: @@ -60,8 +96,8 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-total: 8 + vcpus-allocated: 1 component: 1 inputs: - timestamp: "2023-12-12T00:00:00.000Z" diff --git a/manifests/examples/pipeline-with-aggregate.yml b/manifests/examples/pipeline-with-aggregate.yml index bd3e637ac..73203676b 100644 --- a/manifests/examples/pipeline-with-aggregate.yml +++ b/manifests/examples/pipeline-with-aggregate.yml @@ -7,25 +7,62 @@ aggregation: type: "both" initialize: plugins: - "teads-curve": - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - "sci-e": - path: "@grnsft/if-plugins" - method: SciE + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide + path: "builtin" + global-config: + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh "sci-embodied": path: "builtin" method: SciEmbodied - "sci-o": - path: "@grnsft/if-plugins" - method: SciO + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-energy-kwh", "grid/carbon-intensity"] + output-parameter: "carbon-operational" "sci": path: "builtin" method: Sci global-config: - functional-unit: "requests" + functional-unit: requests # factor to convert per time to per f.unit "sum-carbon": path: "builtin" method: Sum @@ -42,17 +79,18 @@ initialize: end-time: "2023-12-12T00:01:00.000Z" interval: 5 allow-padding: true - "group-by": - path: builtin - method: GroupBy tree: children: child-1: pipeline: - - teads-curve - - sci-e + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -67,8 +105,8 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-total: 8 + vcpus-allocated: 1 inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 @@ -96,10 +134,14 @@ tree: requests: 30 child-2: pipeline: - - teads-curve - - sci-e + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -114,8 +156,8 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 + vcpus-total: 8 + vcpus-allocated: 1 inputs: - timestamp: "2023-12-12T00:00:00.000Z" duration: 1 diff --git a/manifests/examples/pipeline-with-mocks.yml b/manifests/examples/pipeline-with-mocks.yml index 008e319b1..168ebd7fe 100644 --- a/manifests/examples/pipeline-with-mocks.yml +++ b/manifests/examples/pipeline-with-mocks.yml @@ -25,20 +25,57 @@ initialize: cpu/utilization: min: 1 max: 99 - "teads-curve": - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline - "sci-e": - path: "@grnsft/if-plugins" - method: SciE + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide + path: "builtin" + global-config: + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh "sci-embodied": path: "builtin" method: SciEmbodied - "sci-o": - path: "@grnsft/if-plugins" - method: SciO + "operational-carbon": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-energy-kwh", "grid/carbon-intensity"] + output-parameter: "carbon-operational" "sum-carbon": path: "builtin" method: Sum @@ -68,10 +105,14 @@ tree: children: child-1: pipeline: - - teads-curve - - sci-e + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -86,9 +127,8 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 - functional-unit-time: "1 min" + vcpus-total: 8 + vcpus-allocated: 1 inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 @@ -116,10 +156,14 @@ tree: requests: 50 child-2: pipeline: - - teads-curve - - sci-e + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - sci-embodied - - sci-o + - operational-carbon - sum-carbon - time-sync - sci @@ -134,9 +178,8 @@ tree: device/emissions-embodied: 1533.120 # gCO2eq time-reserved: 3600 # 1hr in seconds device/expected-lifespan: 94608000 # 3 years in seconds - resources-reserved: 1 - resources-total: 8 - functional-unit-time: "1 min" + vcpus-total: 8 + vcpus-allocated: 1 inputs: - timestamp: "2023-12-12T00:00:00.000Z" duration: 1 diff --git a/manifests/examples/region-metadata.yml b/manifests/examples/region-metadata.yml new file mode 100644 index 000000000..d43b5bc65 --- /dev/null +++ b/manifests/examples/region-metadata.yml @@ -0,0 +1,26 @@ +name: csv-demo +description: +tags: +initialize: + plugins: + cloud-region-metadata: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/region-metadata.csv + query: + cloud-provider: cloud-provider + cloud-region: cloud-region + output: "*" +tree: + children: + child: + pipeline: + - cloud-region-metadata + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + instance-id: AMD A10-9700 + cloud-provider: "Google Cloud" + cloud-region: "asia-east1" diff --git a/manifests/examples/tdp-finder.yml b/manifests/examples/tdp-finder.yml new file mode 100644 index 000000000..5ce1b5903 --- /dev/null +++ b/manifests/examples/tdp-finder.yml @@ -0,0 +1,23 @@ +name: csv-demo +description: +tags: +initialize: + plugins: + tdp-finder: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/tdp-data-1.csv + query: + name: instance-id + output: "tdp" +tree: + children: + child: + pipeline: + - tdp-finder + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + instance-id: "AMD A10-9700" diff --git a/manifests/examples/zeros.yaml b/manifests/examples/zeros.yaml new file mode 100644 index 000000000..da5541789 --- /dev/null +++ b/manifests/examples/zeros.yaml @@ -0,0 +1,130 @@ +name: generics +description: a pipeline that does arbitrary calculations using our generic arithmetic builtins +tags: +initialize: + plugins: + "sum-zero-and-one": + path: "builtin" + method: Sum + global-config: + input-parameters: + - some-value + - zero-value + output-parameter: one-plus-zero + "sum-zero-and-zero": + path: "builtin" + method: Sum + global-config: + input-parameters: + - zero-value + - zero-value + output-parameter: zero-plus-zero + "subtract-one-and-zero": + path: "builtin" + method: Subtract + global-config: + input-parameters: + - some-value + - zero-value + output-parameter: one-minus-zero + "subtract-zero-and-zero": + path: "builtin" + method: Sum + global-config: + input-parameters: + - zero-value + - zero-value + output-parameter: zero-minus-zero + "subtract-zero-and-one": + path: "builtin" + method: Subtract + global-config: + input-parameters: + - zero-value + - some-value + output-parameter: zero-minus-one + "coefficient-one-times-zero": + path: "builtin" + method: Coefficient + global-config: + input-parameter: zero-value + coefficient: 1 + output-parameter: zero-times-one-coefficient + "coefficient-zero-times-one": + path: "builtin" + method: Coefficient + global-config: + input-parameter: some-value + coefficient: 0 + output-parameter: one-times-zero-coefficient + "coefficient-zero-times-zero": + path: "builtin" + method: Coefficient + global-config: + input-parameter: zero-value + coefficient: 0 + output-parameter: zero-times-zero-coefficient + "multiply-one-times-zero": + path: "builtin" + method: Multiply + global-config: + input-parameters: ["some-value", "zero-value"] + output-parameter: "one-times-zero" + "multiply-zero-times-one": + path: "builtin" + method: Multiply + global-config: + input-parameters: ["zero-value", "zero-value"] + output-parameter: "zero-times-one" + exponent-one-to-zero: + method: Exponent + path: 'builtin' + global-config: + input-parameter: 'some-value' + exponent: 0 + output-parameter: 'one-raised-to-zero-power' + exponent-zero-to-zero: + method: Exponent + path: 'builtin' + global-config: + input-parameter: 'zero-value' + exponent: 0 + output-parameter: 'zero-raised-to-zero-power' + exponent-zero-to-one: + method: Exponent + path: 'builtin' + global-config: + input-parameter: 'zero-value' + exponent: 1 + output-parameter: 'zero-raised-to-first-power' + "sci": + path: "builtin" + method: Sci + global-config: + functional-unit: "zero-value" +tree: + children: + child-1: + pipeline: + - sum-zero-and-one + - sum-zero-and-zero + - subtract-one-and-zero + - subtract-zero-and-zero + - subtract-zero-and-one + - coefficient-one-times-zero + - coefficient-zero-times-one + - coefficient-zero-times-zero + - multiply-one-times-zero + - multiply-zero-times-one + - exponent-one-to-zero + - exponent-zero-to-one + - exponent-zero-to-zero + - sci + config: + defaults: + inputs: + - timestamp: "2023-12-12T00:00:00.000Z" + duration: 1 + some-value: 1 + zero-value: 0 + carbon: 10 diff --git a/manifests/features/aggregate-failure-inalid-metrics.yml b/manifests/features/aggregate-failure-invalid-metrics.yml similarity index 75% rename from manifests/features/aggregate-failure-inalid-metrics.yml rename to manifests/features/aggregate-failure-invalid-metrics.yml index cf2e31bce..7e4692509 100644 --- a/manifests/features/aggregate-failure-inalid-metrics.yml +++ b/manifests/features/aggregate-failure-invalid-metrics.yml @@ -1,5 +1,5 @@ name: Aggregation -description: Apply both `horizontal` and `vertical` aggregations +description: Fails with invalid metric. aggregation: metrics: - 'test' @@ -7,8 +7,14 @@ aggregation: initialize: plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: application: diff --git a/manifests/features/aggregate-failure-missing-metric-in-inputs.yml b/manifests/features/aggregate-failure-missing-metric-in-inputs.yml index 3857d0f76..eeff85290 100644 --- a/manifests/features/aggregate-failure-missing-metric-in-inputs.yml +++ b/manifests/features/aggregate-failure-missing-metric-in-inputs.yml @@ -1,5 +1,5 @@ name: Aggregation -description: Apply both `horizontal` and `vertical` aggregations +description: Fails with missing metric in inputs. aggregation: metrics: - 'cpu/utilization' @@ -7,8 +7,14 @@ aggregation: initialize: plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: application: diff --git a/manifests/features/aggregate-horizontal.yml b/manifests/features/aggregate-horizontal.yml index 114074795..8003c4214 100644 --- a/manifests/features/aggregate-horizontal.yml +++ b/manifests/features/aggregate-horizontal.yml @@ -7,8 +7,14 @@ aggregation: initialize: plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: application: diff --git a/manifests/features/aggregate-vertical.yml b/manifests/features/aggregate-vertical.yml index b37f8e22a..6308a01d0 100644 --- a/manifests/features/aggregate-vertical.yml +++ b/manifests/features/aggregate-vertical.yml @@ -7,8 +7,14 @@ aggregation: initialize: plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: application: diff --git a/manifests/features/aggregate.yml b/manifests/features/aggregate.yml index f320a13ea..04672cf38 100644 --- a/manifests/features/aggregate.yml +++ b/manifests/features/aggregate.yml @@ -7,8 +7,14 @@ aggregation: initialize: plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: application: diff --git a/manifests/integrations/cloud-metadata-divide-boavizta.yml b/manifests/integrations/cloud-metadata-divide.yml similarity index 67% rename from manifests/integrations/cloud-metadata-divide-boavizta.yml rename to manifests/integrations/cloud-metadata-divide.yml index cafd1467b..032206474 100644 --- a/manifests/integrations/cloud-metadata-divide-boavizta.yml +++ b/manifests/integrations/cloud-metadata-divide.yml @@ -4,8 +4,14 @@ tags: initialize: plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] divide: method: Divide path: "builtin" @@ -13,19 +19,12 @@ initialize: numerator: vcpus-allocated denominator: 2 output: cpu/number-cores - boavizta-cpu: - method: BoaviztaCpuOutput - path: "@grnsft/if-unofficial-plugins" - global-config: - allocation: LINEAR - verbose: true tree: children: child: pipeline: - cloud-metadata - divide - - boavizta-cpu config: divide: defaults: diff --git a/manifests/integrations/instance-metadata/failure-invalid-instance-type.yaml b/manifests/integrations/instance-metadata/failure-invalid-instance-type.yaml new file mode 100644 index 000000000..810dee072 --- /dev/null +++ b/manifests/integrations/instance-metadata/failure-invalid-instance-type.yaml @@ -0,0 +1,33 @@ +name: instance-metadata +description: +tags: +initialize: + plugins: + cloud-instance-metadata: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-azure-instances.csv + query: + instance-class: "cloud/instance-type" + output: "*" + extract-processor-name: + method: Regex + path: "builtin" + global-config: + parameter: cpu-model-name + match: /^([^,])+/g + output: cpu/name +tree: + children: + child: + pipeline: + - cloud-instance-metadata + - extract-processor-name + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + cloud/provider: gcp + cloud/region: asia-east + cloud/instance-type: m6 \ No newline at end of file diff --git a/manifests/integrations/instance-metadata/failure-invalid-provider.yaml b/manifests/integrations/instance-metadata/failure-invalid-provider.yaml new file mode 100644 index 000000000..a362132e7 --- /dev/null +++ b/manifests/integrations/instance-metadata/failure-invalid-provider.yaml @@ -0,0 +1,33 @@ +name: instance-metadata +description: +tags: +initialize: + plugins: + cloud-instance-metadata: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-azure-instances.csv + query: + instance-class: "cloud/provider" + output: "*" + extract-processor-name: + method: Regex + path: "builtin" + global-config: + parameter: cpu-model-name + match: /^([^,])+/g + output: cpu/name +tree: + children: + child: + pipeline: + - cloud-instance-metadata + - extract-processor-name + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + cloud/provider: 6568 + cloud/region: asia-east + cloud/instance-type: Standard_A1_v2 diff --git a/manifests/integrations/instance-metadata/failure-missing-queried-input.yml b/manifests/integrations/instance-metadata/failure-missing-queried-input.yml new file mode 100644 index 000000000..37f4001ae --- /dev/null +++ b/manifests/integrations/instance-metadata/failure-missing-queried-input.yml @@ -0,0 +1,33 @@ +name: instance-metadata +description: +tags: +initialize: + plugins: + cloud-instance-metadata: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-azure-instances.csv + query: + instance-class: "cloud/provider" + output: "*" + extract-processor-name: + method: Regex + path: "builtin" + global-config: + parameter: cpu-model-name + match: /^([^,])+/g + output: cpu/name +tree: + children: + child: + pipeline: + - cloud-instance-metadata + - extract-processor-name + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + # cloud/provider: gcp + cloud/region: asia-east + cloud/instance-type: Standard_A1_v2 \ No newline at end of file diff --git a/manifests/integrations/instance-metadata/success.yml b/manifests/integrations/instance-metadata/success.yml new file mode 100644 index 000000000..d95115896 --- /dev/null +++ b/manifests/integrations/instance-metadata/success.yml @@ -0,0 +1,33 @@ +name: instance-metadata +description: +tags: +initialize: + plugins: + cloud-instance-metadata: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-azure-instances.csv + query: + instance-class: "cloud/instance-type" + output: "*" + extract-processor-name: + method: Regex + path: "builtin" + global-config: + parameter: cpu-model-name + match: /^([^,])+/g + output: cpu/name +tree: + children: + child: + pipeline: + - cloud-instance-metadata + - extract-processor-name + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + cloud/provider: gcp + cloud/region: asia-east + cloud/instance-type: Standard_A1_v2 \ No newline at end of file diff --git a/manifests/integrations/mock-obs-group-by-cloud-meta.yml b/manifests/integrations/mock-obs-group-by-cloud-meta.yml index b0d43f2a5..6b6196cab 100644 --- a/manifests/integrations/mock-obs-group-by-cloud-meta.yml +++ b/manifests/integrations/mock-obs-group-by-cloud-meta.yml @@ -14,8 +14,14 @@ initialize: input-parameters: ['cpu/energy', 'grid/carbon-intensity'] output-parameter: 'carbon' cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-azure-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-tdp'] mock-observations: path: 'builtin' method: MockObservations diff --git a/manifests/integrations/mock-obs-time-sync.yml b/manifests/integrations/mock-obs-time-sync.yml index 727caf363..f1f176eb3 100644 --- a/manifests/integrations/mock-obs-time-sync.yml +++ b/manifests/integrations/mock-obs-time-sync.yml @@ -21,11 +21,48 @@ initialize: cpu/utilization: min: 1 max: 99 - "teads-curve": - path: "@grnsft/if-unofficial-plugins" - method: TeadsCurve + "interpolate": + method: Interpolation + path: 'builtin' global-config: - interpolation: spline + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: 'cpu/utilization' + output-parameter: 'cpu-factor' + "cpu-factor-to-wattage": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-factor", "cpu/thermal-design-power"] + output-parameter: "cpu-wattage" + "wattage-times-duration": + method: Multiply + path: builtin + global-config: + input-parameters: ["cpu-wattage", "duration"] + output-parameter: "cpu-wattage-times-duration" + "wattage-to-energy-kwh": + method: Divide + path: "builtin" + global-config: + numerator: cpu-wattage-times-duration + denominator: 3600000 + output: cpu-energy-raw + "calculate-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: vcpus-total + denominator: vcpus-allocated + output: vcpu-ratio + "correct-cpu-energy-for-vcpu-ratio": + method: Divide + path: "builtin" + global-config: + numerator: cpu-energy-raw + denominator: vcpu-ratio + output: cpu-energy-kwh "time-sync": method: TimeSync path: "builtin" @@ -38,10 +75,18 @@ tree: children: child-1: pipeline: - - teads-curve + # - mock-observations + - interpolate + - cpu-factor-to-wattage + - wattage-times-duration + - wattage-to-energy-kwh + - calculate-vcpu-ratio + - correct-cpu-energy-for-vcpu-ratio - time-sync defaults: cpu/thermal-design-power: 100 + vcpus-total: 8 + vcpus-allocated: 1 inputs: - timestamp: "2023-12-12T00:00:00.000Z" cloud/instance-type: A1 diff --git a/manifests/plugins/cloud-metadata/failure-invalid-instance-type.yaml b/manifests/plugins/csv-lookup/cloud-metadata/failure-invalid-instance-type.yaml similarity index 57% rename from manifests/plugins/cloud-metadata/failure-invalid-instance-type.yaml rename to manifests/plugins/csv-lookup/cloud-metadata/failure-invalid-instance-type.yaml index 9bb4581ef..fa55623c2 100644 --- a/manifests/plugins/cloud-metadata/failure-invalid-instance-type.yaml +++ b/manifests/plugins/csv-lookup/cloud-metadata/failure-invalid-instance-type.yaml @@ -2,11 +2,17 @@ name: cloud-metadata description: cloud/instance-type instance type is not supported in the cloud vendor tags: initialize: - outputs: ['yaml'] + # outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: child: diff --git a/manifests/plugins/cloud-metadata/failure-invalid-vendor.yaml b/manifests/plugins/csv-lookup/cloud-metadata/failure-invalid-vendor.yaml similarity index 59% rename from manifests/plugins/cloud-metadata/failure-invalid-vendor.yaml rename to manifests/plugins/csv-lookup/cloud-metadata/failure-invalid-vendor.yaml index 9e2ebe4f2..c474c91fe 100644 --- a/manifests/plugins/cloud-metadata/failure-invalid-vendor.yaml +++ b/manifests/plugins/csv-lookup/cloud-metadata/failure-invalid-vendor.yaml @@ -5,8 +5,14 @@ initialize: #outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: child: diff --git a/manifests/plugins/cloud-metadata/failure-missing-cloud-vendor.yml b/manifests/plugins/csv-lookup/cloud-metadata/failure-missing-cloud-vendor.yml similarity index 56% rename from manifests/plugins/cloud-metadata/failure-missing-cloud-vendor.yml rename to manifests/plugins/csv-lookup/cloud-metadata/failure-missing-cloud-vendor.yml index 9b80c4301..33c5cc4b1 100644 --- a/manifests/plugins/cloud-metadata/failure-missing-cloud-vendor.yml +++ b/manifests/plugins/csv-lookup/cloud-metadata/failure-missing-cloud-vendor.yml @@ -2,11 +2,17 @@ name: cloud-metadata description: failing because cloud/vendor is not provided tags: initialize: - outputs: ['yaml'] + # outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: child: diff --git a/manifests/plugins/cloud-metadata/success.yml b/manifests/plugins/csv-lookup/cloud-metadata/success.yml similarity index 57% rename from manifests/plugins/cloud-metadata/success.yml rename to manifests/plugins/csv-lookup/cloud-metadata/success.yml index dec29460d..bd3cfdbb7 100644 --- a/manifests/plugins/cloud-metadata/success.yml +++ b/manifests/plugins/csv-lookup/cloud-metadata/success.yml @@ -5,8 +5,14 @@ initialize: # outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] tree: children: child: diff --git a/manifests/plugins/csv-lookup/failure-missing-column.yml b/manifests/plugins/csv-lookup/region-metadata/failure-missing-column.yml similarity index 100% rename from manifests/plugins/csv-lookup/failure-missing-column.yml rename to manifests/plugins/csv-lookup/region-metadata/failure-missing-column.yml diff --git a/manifests/plugins/csv-lookup/failure-missing-output.yml b/manifests/plugins/csv-lookup/region-metadata/failure-missing-output.yml similarity index 100% rename from manifests/plugins/csv-lookup/failure-missing-output.yml rename to manifests/plugins/csv-lookup/region-metadata/failure-missing-output.yml diff --git a/manifests/plugins/csv-lookup/success-renaming.yml b/manifests/plugins/csv-lookup/region-metadata/success-renaming.yml similarity index 100% rename from manifests/plugins/csv-lookup/success-renaming.yml rename to manifests/plugins/csv-lookup/region-metadata/success-renaming.yml diff --git a/manifests/plugins/csv-lookup/success.yml b/manifests/plugins/csv-lookup/region-metadata/success.yml similarity index 94% rename from manifests/plugins/csv-lookup/success.yml rename to manifests/plugins/csv-lookup/region-metadata/success.yml index f57b4718a..c6fc07d7b 100644 --- a/manifests/plugins/csv-lookup/success.yml +++ b/manifests/plugins/csv-lookup/region-metadata/success.yml @@ -23,4 +23,4 @@ tree: - timestamp: 2023-08-06T00:00 duration: 3600 cloud/provider: Google Cloud - cloud/region: asia-east-1 + cloud/region: asia-east1 diff --git a/manifests/plugins/csv-lookup/tdp-finder/failure-missing-input-param.yml b/manifests/plugins/csv-lookup/tdp-finder/failure-missing-input-param.yml new file mode 100644 index 000000000..b14e57d1d --- /dev/null +++ b/manifests/plugins/csv-lookup/tdp-finder/failure-missing-input-param.yml @@ -0,0 +1,23 @@ +name: tdp-finder +description: failure with `inputs` missing `physical-processor` param +tags: +initialize: + # outputs: ['yaml'] + plugins: + tdp-finder: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/tdp-data-1.csv + query: + name: physical-processor + output: "tdp" +tree: + children: + child: + pipeline: + - tdp-finder + config: + inputs: + - timestamp: 2023-07-06T00:00 + duration: 300 diff --git a/manifests/plugins/csv-lookup/tdp-finder/failure-unsupported-physical-processor.yml b/manifests/plugins/csv-lookup/tdp-finder/failure-unsupported-physical-processor.yml new file mode 100644 index 000000000..47f392fcc --- /dev/null +++ b/manifests/plugins/csv-lookup/tdp-finder/failure-unsupported-physical-processor.yml @@ -0,0 +1,24 @@ +name: tdp-finder +description: successful path +tags: +initialize: + # outputs: ['yaml'] + plugins: + tdp-finder: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/tdp-data-1.csv + query: + name: physical-processor + output: "tdp" +tree: + children: + child: + pipeline: + - tdp-finder + config: + inputs: + - timestamp: 2023-07-06T00:00 + duration: 300 + physical-processor: AMD 302 diff --git a/manifests/plugins/csv-lookup/tdp-finder/success.yml b/manifests/plugins/csv-lookup/tdp-finder/success.yml new file mode 100644 index 000000000..42545b0df --- /dev/null +++ b/manifests/plugins/csv-lookup/tdp-finder/success.yml @@ -0,0 +1,24 @@ +name: tdp-finder +description: successful path +tags: +initialize: + # outputs: ['yaml'] + plugins: + tdp-finder: + method: CSVLookup + path: "builtin" + global-config: + filepath: https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/tdp-data-1.csv + query: + name: physical-processor + output: "tdp" +tree: + children: + child: + pipeline: + - tdp-finder + config: + inputs: + - timestamp: 2023-07-06T00:00 + duration: 300 + physical-processor: AMD 3020e diff --git a/manifests/plugins/divide/failure-denominator-equal-zero.yml b/manifests/plugins/divide/failure-denominator-equal-zero.yml index af5ed2ae0..4022d4797 100644 --- a/manifests/plugins/divide/failure-denominator-equal-zero.yml +++ b/manifests/plugins/divide/failure-denominator-equal-zero.yml @@ -5,8 +5,14 @@ initialize: # outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] divide: method: Divide path: "builtin" @@ -14,19 +20,12 @@ initialize: numerator: vcpus-allocated denominator: 0 output: cpu/number-cores - boavizta-cpu: - method: BoaviztaCpuOutput - path: "@grnsft/if-unofficial-plugins" - global-config: - allocation: LINEAR - verbose: true tree: children: child: pipeline: - cloud-metadata - divide - - boavizta-cpu config: divide: defaults: diff --git a/manifests/plugins/divide/failure-invalid-config-denominator.yml b/manifests/plugins/divide/failure-invalid-config-denominator.yml index 063274bc0..91129714a 100644 --- a/manifests/plugins/divide/failure-invalid-config-denominator.yml +++ b/manifests/plugins/divide/failure-invalid-config-denominator.yml @@ -5,8 +5,14 @@ initialize: outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] divide: method: Divide path: "builtin" @@ -14,19 +20,12 @@ initialize: numerator: vcpus-allocated denominator: 'vcpus' output: cpu/number-cores - boavizta-cpu: - method: BoaviztaCpuOutput - path: "@grnsft/if-unofficial-plugins" - global-config: - allocation: LINEAR - verbose: true tree: children: child: pipeline: - cloud-metadata - divide - - boavizta-cpu config: divide: defaults: diff --git a/manifests/plugins/divide/failure-missing-numerator.yml b/manifests/plugins/divide/failure-missing-numerator.yml index 7f03d5eba..7cf123976 100644 --- a/manifests/plugins/divide/failure-missing-numerator.yml +++ b/manifests/plugins/divide/failure-missing-numerator.yml @@ -5,8 +5,14 @@ initialize: # outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] divide: method: Divide path: "builtin" @@ -14,19 +20,12 @@ initialize: #numerator: vcpus-allocated denominator: 2 output: cpu/number-cores - boavizta-cpu: - method: BoaviztaCpuOutput - path: "@grnsft/if-unofficial-plugins" - global-config: - allocation: LINEAR - verbose: true tree: children: child: pipeline: - cloud-metadata - divide - - boavizta-cpu config: divide: defaults: diff --git a/manifests/plugins/divide/success.yml b/manifests/plugins/divide/success.yml index ed75a7c5c..6a32d7e2c 100644 --- a/manifests/plugins/divide/success.yml +++ b/manifests/plugins/divide/success.yml @@ -2,11 +2,17 @@ name: divide description: success path tags: initialize: -# outputs: ['yaml'] + # outputs: ['yaml'] plugins: cloud-metadata: - method: CloudMetadata - path: "@grnsft/if-plugins" + path: builtin + method: CSVLookup + global-config: + filepath: >- + https://raw.githubusercontent.com/Green-Software-Foundation/if-data/main/cloud-metdata-aws-instances.csv + query: + instance-class: cloud/instance-type + output: ['cpu-cores-utilized', 'vcpus-allocated'] divide: method: Divide path: "builtin" @@ -14,19 +20,12 @@ initialize: numerator: vcpus-allocated denominator: 2 output: cpu/number-cores - boavizta-cpu: - method: BoaviztaCpuOutput - path: "@grnsft/if-unofficial-plugins" - global-config: - allocation: LINEAR - verbose: true tree: children: child: pipeline: - cloud-metadata - divide - - boavizta-cpu config: divide: defaults: diff --git a/manifests/plugins/exponent/success.yml b/manifests/plugins/exponent/success.yml new file mode 100644 index 000000000..c123b3a24 --- /dev/null +++ b/manifests/plugins/exponent/success.yml @@ -0,0 +1,26 @@ +name: exponent demo +description: +tags: +initialize: + # outputs: + # - yaml + plugins: + exponent: + method: Exponent + path: 'builtin' + global-config: + input-parameter: 'cpu/energy' + exponent: 2 + output-parameter: 'energy' +tree: + children: + child: + pipeline: + - exponent + config: + exponent: + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.001 + network/energy: 0.001 diff --git a/manifests/plugins/interpolation/success.yml b/manifests/plugins/interpolation/success.yml new file mode 100644 index 000000000..35d750e48 --- /dev/null +++ b/manifests/plugins/interpolation/success.yml @@ -0,0 +1,24 @@ +name: interpolation-demo +description: simple demo of interpolation plugin +tags: +initialize: + plugins: + interpolation: + method: Interpolation + path: "builtin" + global-config: + method: linear + x: [0, 10, 50, 100] + y: [0.12, 0.32, 0.75, 1.02] + input-parameter: "cpu/utilization" + output-parameter: "result" + +tree: + children: + child: + pipeline: + - interpolation + inputs: + - timestamp: 2023-07-06T00:00 + duration: 3600 + cpu/utilization: 45 diff --git a/manifests/plugins/sci/failure-missing-input-param.yml b/manifests/plugins/sci/failure-missing-input-param.yml index 44ced7374..54514a8e5 100644 --- a/manifests/plugins/sci/failure-missing-input-param.yml +++ b/manifests/plugins/sci/failure-missing-input-param.yml @@ -8,17 +8,13 @@ initialize: kind: plugin method: Sci path: "builtin" - # global-config: - # functional-unit: requests + global-config: + functional-unit: requests tree: children: child: pipeline: - sci - config: - sci: - functional-unit-time: 1 sec - functional-unit: requests # factor to convert per time to per f.unit inputs: - timestamp: 2023-07-06T00:00 duration: 3600 diff --git a/manifests/plugins/sci/success.yml b/manifests/plugins/sci/success.yml index c7e6dbde6..cddfe5755 100644 --- a/manifests/plugins/sci/success.yml +++ b/manifests/plugins/sci/success.yml @@ -16,9 +16,6 @@ tree: pipeline: - sci config: - sci: - functional-unit-time: 1 sec - functional-unit: requests # factor to convert per time to per f.unit inputs: - timestamp: 2023-07-06T00:00 duration: 3600 diff --git a/manifests/plugins/subtract/success.yml b/manifests/plugins/subtract/success.yml new file mode 100644 index 000000000..276128171 --- /dev/null +++ b/manifests/plugins/subtract/success.yml @@ -0,0 +1,25 @@ +name: subtract demo +description: +tags: +initialize: + outputs: + - yaml + plugins: + subtract: + method: Subtract + path: 'builtin' + global-config: + input-parameters: ['cpu/energy', 'network/energy'] + output-parameter: 'energy/diff' +tree: + children: + child: + pipeline: + - subtract + config: + subtract: + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + cpu/energy: 0.003 + network/energy: 0.001 \ No newline at end of file diff --git a/manifests/plugins/sum/success.yml b/manifests/plugins/sum/success.yml index fc0661bf5..454efc9e0 100644 --- a/manifests/plugins/sum/success.yml +++ b/manifests/plugins/sum/success.yml @@ -2,7 +2,7 @@ name: sum description: successful path tags: initialize: - outputs: ['yaml'] + # outputs: ['yaml'] plugins: sum: method: Sum diff --git a/manifests/plugins/tdp-finder/failure-missing-input-param.yml b/manifests/plugins/tdp-finder/failure-missing-input-param.yml deleted file mode 100644 index 7877b56ce..000000000 --- a/manifests/plugins/tdp-finder/failure-missing-input-param.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: tdp-finder -description: failure with `inputs` missing `physical-processor` param -tags: -initialize: - outputs: ['yaml'] - plugins: - tdp-finder: # a model that returns an embodied value given the sci embodied attribution equation. - method: TdpFinder - path: "@grnsft/if-plugins" -tree: - children: - child: - pipeline: - - tdp-finder - config: - inputs: - - timestamp: 2023-07-06T00:00 - duration: 300 diff --git a/manifests/plugins/tdp-finder/failure-unsupported-physical-processor.yml b/manifests/plugins/tdp-finder/failure-unsupported-physical-processor.yml deleted file mode 100644 index 56c5c93a5..000000000 --- a/manifests/plugins/tdp-finder/failure-unsupported-physical-processor.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: tdp-finder -description: successful path -tags: -initialize: - outputs: ['yaml'] - plugins: - tdp-finder: # a model that returns an embodied value given the sci embodied attribution equation. - method: TdpFinder - path: "@grnsft/if-plugins" -tree: - children: - child: - pipeline: - - tdp-finder - config: - inputs: - - timestamp: 2023-07-06T00:00 - duration: 300 - physical-processor: AMD 302 diff --git a/manifests/plugins/tdp-finder/success.yml b/manifests/plugins/tdp-finder/success.yml deleted file mode 100644 index 26f905ecd..000000000 --- a/manifests/plugins/tdp-finder/success.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: tdp-finder -description: successful path -tags: -initialize: - outputs: ['yaml'] - plugins: - tdp-finder: # a model that returns an embodied value given the sci embodied attribution equation. - method: TdpFinder - path: "@grnsft/if-plugins" -tree: - children: - child: - pipeline: - - tdp-finder - config: - inputs: - - timestamp: 2023-07-06T00:00 - duration: 300 - physical-processor: AMD 3020e diff --git a/package-lock.json b/package-lock.json index 1c2c47735..0cd6305c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@grnsft/if", - "version": "0.4.0", + "version": "0.5.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@grnsft/if", - "version": "0.4.0", + "version": "0.5.0-beta.0", "license": "MIT", "dependencies": { "@commitlint/cli": "^18.6.0", "@commitlint/config-conventional": "^18.6.0", + "@grnsft/if-core": "^0.0.10", "axios": "^1.7.2", "csv-parse": "^5.5.6", "csv-stringify": "^6.4.6", @@ -23,8 +24,10 @@ "zod": "^3.22.4" }, "bin": { - "ie": "build/index.js", - "if-diff": "build/diff.js" + "if-check": "build/check.js", + "if-diff": "build/diff.js", + "if-env": "build/env.js", + "if-run": "build/index.js" }, "devDependencies": { "@babel/core": "^7.22.10", @@ -35,6 +38,7 @@ "@types/luxon": "^3.4.2", "@types/node": "^20.8.9", "axios-mock-adapter": "^1.22.0", + "cross-env": "7.0.3", "fixpack": "^4.0.0", "gts": "^5.0.0", "husky": "^8.0.0", @@ -1179,6 +1183,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@grnsft/if-core": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@grnsft/if-core/-/if-core-0.0.10.tgz", + "integrity": "sha512-WHCdr7H/dFO9gT5fbjrthjOU+4RoLZ5P1F84pbGwJiKLmcU7dvYRuNQKDVIQQ7YJfZl76KSaS7sYgqA+QG8Wpw==", + "dependencies": { + "typescript": "^5.1.6" + }, + "engines": { + "node": ">=18", + "npm": ">=8" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "dev": true, @@ -3758,6 +3774,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "license": "MIT", diff --git a/package.json b/package.json index c6db34155..e01715c14 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "@grnsft/if", "description": "Impact Framework", - "version": "0.4.0", + "version": "0.5.0-beta.0", "author": { "name": "Green Software Foundation", "email": "info@gsf.com" }, "bin": { - "ie": "./build/index.js", - "if-diff": "./build/diff.js" + "if-diff": "./build/diff.js", + "if-run": "./build/index.js", + "if-env": "./build/env.js", + "if-check": "./build/check.js" }, "bugs": { "url": "https://github.com/Green-Software-Foundation/if/issues/new?assignees=&labels=feedback&projects=&template=feedback.md&title=Feedback+-+" @@ -16,6 +18,7 @@ "dependencies": { "@commitlint/cli": "^18.6.0", "@commitlint/config-conventional": "^18.6.0", + "@grnsft/if-core": "^0.0.10", "axios": "^1.7.2", "csv-parse": "^5.5.6", "csv-stringify": "^6.4.6", @@ -36,6 +39,7 @@ "@types/luxon": "^3.4.2", "@types/node": "^20.8.9", "axios-mock-adapter": "^1.22.0", + "cross-env": "7.0.3", "fixpack": "^4.0.0", "gts": "^5.0.0", "husky": "^8.0.0", @@ -72,8 +76,10 @@ "coverage": "jest --verbose --coverage --testPathPattern=src/__tests__/unit", "fix": "gts fix", "fix:package": "fixpack", - "ie": "npx ts-node src/index.ts", + "if-check": "cross-env CURRENT_DIR=$(node -p \"process.env.INIT_CWD\") npx ts-node src/check.ts", "if-diff": "npx ts-node src/diff.ts", + "if-env": "cross-env CURRENT_DIR=$(node -p \"process.env.INIT_CWD\") npx ts-node src/env.ts", + "if-run": "npx ts-node src/index.ts", "lint": "gts lint", "pre-commit": "lint-staged", "prepare": "husky install", diff --git a/scripts/run-yamls.sh b/scripts/run-yamls.sh index 11371af66..fbe394c6b 100644 --- a/scripts/run-yamls.sh +++ b/scripts/run-yamls.sh @@ -4,5 +4,5 @@ echo 'Running all manifests' for f in ./examples/manifests/*.yml; do echo "Processing $f file..."; - npm run ie -- --manifest $f + npm run if-run -- --manifest $f done diff --git a/src/__mocks__/builtins/export-csv.ts b/src/__mocks__/builtins/export-csv.ts index e8fc2a26f..1f0e00e73 100644 --- a/src/__mocks__/builtins/export-csv.ts +++ b/src/__mocks__/builtins/export-csv.ts @@ -14,7 +14,6 @@ export const tree = { 'device/expected-lifespan': 94608000, 'resources-reserved': 1, 'resources-total': 8, - 'functional-unit-time': '1 min', }, inputs: [ { @@ -43,7 +42,6 @@ export const tree = { 'device/expected-lifespan': 94608000, 'resources-reserved': 1, 'resources-total': 8, - 'functional-unit-time': '1 min', 'cpu/energy': 0.000008888888888888888, "carbon-plus-energy'": 10.000008888888889, 'carbon-embodied': 0.0000020256215119228817, @@ -65,7 +63,6 @@ export const tree = { 'device/expected-lifespan': 94608000, 'resources-reserved': 1, 'resources-total': 8, - 'functional-unit-time': '1 min', }, inputs: [ { @@ -94,7 +91,6 @@ export const tree = { 'device/expected-lifespan': 94608000, 'resources-reserved': 1, 'resources-total': 8, - 'functional-unit-time': '1 min', 'cpu/energy': 0.00001650338753387534, "carbon-plus-energy'": 10.000016503387533, 'carbon-embodied': 0.0000020256215119228817, @@ -154,7 +150,6 @@ export const context: Context = { method: 'Sci', 'global-config': { 'functional-unit': 'requests', - 'functional-unit-time': '1 minute', }, }, }, diff --git a/src/__mocks__/fs/index.ts b/src/__mocks__/fs/index.ts index da8438c8b..6d4f61b8b 100644 --- a/src/__mocks__/fs/index.ts +++ b/src/__mocks__/fs/index.ts @@ -1,10 +1,20 @@ import * as YAML from 'js-yaml'; +import * as fs from 'fs'; +import * as fsAsync from 'fs/promises'; +import * as path from 'path'; export const readFile = async (filePath: string) => { + /** mock for util/npm */ + if (filePath.includes('package.json-npm')) { + const updatedPath = filePath.replace('-npm', ''); + return fs.readFileSync(updatedPath, 'utf8'); + } + /** mock for util/json */ if (filePath.includes('json-reject')) { return Promise.reject(new Error('rejected')); } + if (filePath.includes('json')) { if (filePath.includes('param')) { return JSON.stringify({ @@ -28,6 +38,13 @@ export const readFile = async (filePath: string) => { throw new Error('file not found'); } + if (filePath.includes('fail-csv-reader.csv')) { + return ` +cpu-cores-available,≈ç≈¬˚∆∑∂´®øˆ´cpu-cores-utilized, ---- cpu-manufacturer,cpu-model-name,cpu-tdp,gpu-count,gpu-model-name,Hardware Information on AWS Documentation & Comments,instance-class,instance-storage,memory-available,platform-memory,release-date,storage-drives +16,8,AWS,AWS Graviton +16,16,AWS,AWS Graviton,150.00,N/A,N/A,AWS Graviton (ARM),a1.4xlarge,EBS-Only,32,32,November 2018,`; + } + /** * Used for csv lookup plugin. */ @@ -73,16 +90,98 @@ cpu-cores-available,cpu-cores-utilized,cpu-manufacturer,cpu-model-name,cpu-tdp,g export const mkdir = (dirPath: string) => dirPath; export const writeFile = async (pathToFile: string, content: string) => { - if (pathToFile === 'reject') { - throw new Error('Wrong file path'); + if (pathToFile.includes('package.json-npm1')) { + const updatedPath = pathToFile.replace('-npm1', ''); + const fileContent = await fsAsync.readFile(updatedPath, 'utf8'); + const fileContentObject = JSON.parse(fileContent); + const parsedContent = JSON.parse(content); + + for (const property in fileContentObject) { + expect(parsedContent).toHaveProperty(property); + } + } else if (pathToFile.includes('package.json-npm')) { + const updatedPath = pathToFile.replace('-npm', ''); + const fileContent = await fsAsync.readFile(updatedPath, 'utf8'); + + expect(content).toBe(fileContent); + } else if (pathToFile.includes('/manifest.yml')) { + const templateManifest = path.resolve( + __dirname, + '../../config/env-template.yml' + ); + const fileContent = await fsAsync.readFile(templateManifest, 'utf8'); + + expect(content).toBe(fileContent); + } else { + if (pathToFile === 'reject') { + throw new Error('Wrong file path'); + } + + const mockPathToFile = 'mock-pathToFile'; + const mockContent = { + name: 'mock-name', + }; + const mockObject = YAML.dump(mockContent, {noRefs: true}); + + expect(pathToFile).toBe(mockPathToFile); + expect(content).toBe(mockObject); + } +}; + +export const stat = async (filePath: string) => { + if (filePath === 'true') { + return true; + } else { + throw new Error('File not found.'); } +}; - const mockPathToFile = 'mock-pathToFile'; - const mockContent = { - name: 'mock-name', - }; - const mockObject = YAML.dump(mockContent, {noRefs: true}); +export const access = async (directoryPath: string) => { + if (directoryPath === 'true') { + return true; + } else { + throw new Error('Directory not found.'); + } +}; - expect(pathToFile).toBe(mockPathToFile); - expect(content).toBe(mockObject); +export const unlink = async (filePath: string) => { + if (filePath === 'true') { + return; + } else { + throw new Error('File not found.'); + } +}; + +export const readdir = (directoryPath: string) => { + if (directoryPath.includes('mock-empty-directory')) { + return []; + } + + if (directoryPath.includes('mock-directory')) { + return ['file1.yaml', 'file2.yml', 'file3.txt']; + } + + if (directoryPath.includes('mock-sub-directory')) { + return ['subdir/file2.yml', 'file1.yaml']; + } + + return []; +}; + +export const lstat = (filePath: string) => { + if ( + filePath.includes('mock-directory') || + filePath.includes('mock-sub-directory/subdir') + ) { + return { + isDirectory: () => true, + }; + } + + if (filePath.includes('mock-file')) { + return { + isDirectory: () => false, + }; + } + return; }; diff --git a/src/__mocks__/mock-manifest.yaml b/src/__mocks__/mock-manifest.yaml new file mode 100644 index 000000000..1e38c1b13 --- /dev/null +++ b/src/__mocks__/mock-manifest.yaml @@ -0,0 +1,72 @@ +name: template manifest +description: auto-generated template +tags: null +initialize: + plugins: + memory-energy-from-memory-util: + path: builtin + method: Coefficient + global-config: + input-parameter: memory/utilization + coefficient: 0.0001 + output-parameter: memory/energy + outputs: + - yaml +execution: + command: >- + /Users/manushak/.npm/_npx/1bf7c3c15bf47d04/node_modules/.bin/ts-node + /Users/manushak/Documents/Projects/Green-Software/if/src/index.ts -m + ./src/env-template.yml -o ./manifests/outputs/template + environment: + if-version: 0.4.0 + os: macOS + os-version: 13.6.6 + node-version: 20.12.2 + date-time: 2024-06-18T08:39:55.771Z (UTC) + dependencies: + - "@babel/core@7.22.10" + - "@babel/preset-typescript@7.23.3" + - "@commitlint/cli@18.6.0" + - "@commitlint/config-conventional@18.6.0" + - "@grnsft/if-core@0.0.3" + - "@jest/globals@29.7.0" + - "@types/jest@29.5.8" + - "@types/js-yaml@4.0.9" + - "@types/luxon@3.4.2" + - "@types/node@20.9.0" + - axios-mock-adapter@1.22.0 + - axios@1.7.2 + - cross-env@7.0.3 + - csv-parse@5.5.6 + - csv-stringify@6.4.6 + - fixpack@4.0.0 + - gts@5.2.0 + - husky@8.0.3 + - jest@29.7.0 + - js-yaml@4.1.0 + - lint-staged@15.2.2 + - luxon@3.4.4 + - release-it@16.3.0 + - rimraf@5.0.5 + - ts-command-line-args@2.5.1 + - ts-jest@29.1.1 + - typescript-cubic-spline@1.0.1 + - typescript@5.2.2 + - winston@3.11.0 + - zod@3.22.4 + status: success +tree: + children: + child: + pipeline: + - memory-energy-from-memory-util + config: null + inputs: + - timestamp: 2023-12-12T00:00:00.000Z + duration: 3600 + memory/utilization: 10 + outputs: + - timestamp: 2023-12-12T00:00:00.000Z + duration: 3600 + memory/utilization: 10 + memory/energy: 0.001 diff --git a/src/__mocks__/plugin/lib/mockavizta/index.ts b/src/__mocks__/plugin/lib/mockavizta/index.ts index 163a0cd24..54feae6a2 100644 --- a/src/__mocks__/plugin/lib/mockavizta/index.ts +++ b/src/__mocks__/plugin/lib/mockavizta/index.ts @@ -1,4 +1,4 @@ -import {PluginParams} from '../../../../types/interface'; +import {PluginParams} from '@grnsft/if-core/types'; /** * Mock model for testing. diff --git a/src/__tests__/integration/scenarios/sci-e.ts b/src/__tests__/integration/scenarios/sci-e.ts index 9fb808341..0342240c2 100644 --- a/src/__tests__/integration/scenarios/sci-e.ts +++ b/src/__tests__/integration/scenarios/sci-e.ts @@ -36,7 +36,7 @@ describe('integration/sci-e', () => { await saveYamlFileAs(file, absoluteManifestPath); // save yaml uses absolute path const response = ( await execPromise( - `npm run ie -- --manifest ${relativeManifestPath} --stdout` + `npm run if-run -- --manifest ${relativeManifestPath} --stdout` ) ).stdout; // exec promise uses relative path diff --git a/src/__tests__/unit/builtins/CommonGenerator.test.ts b/src/__tests__/unit/builtins/CommonGenerator.test.ts index 881aa810e..b0c165d1a 100644 --- a/src/__tests__/unit/builtins/CommonGenerator.test.ts +++ b/src/__tests__/unit/builtins/CommonGenerator.test.ts @@ -1,12 +1,13 @@ -import {KeyValuePair} from '../../../types/common'; - -import {ERRORS} from '../../../util/errors'; +import {ERRORS} from '@grnsft/if-core/utils'; import {CommonGenerator} from '../../../builtins/mock-observations/helpers/common-generator'; -const {InputValidationError} = ERRORS; +import {STRINGS} from '../../../config'; + +const {GlobalConfigError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; -describe('lib/mock-observations/CommonGenerator: ', () => { +describe('builtins/mock-observations/CommonGenerator: ', () => { describe('initialize: ', () => { it('throws an error when config is not empty object.', async () => { const commonGenerator = CommonGenerator({}); @@ -16,18 +17,14 @@ describe('lib/mock-observations/CommonGenerator: ', () => { try { commonGenerator.next([]); } catch (error) { - expect(error).toEqual( - new InputValidationError( - 'CommonGenerator: Config must not be null or empty.' - ) - ); + expect(error).toEqual(new GlobalConfigError(MISSING_GLOBAL_CONFIG)); } }); }); describe('next(): ', () => { it('returns a result with valid data.', async () => { - const config: KeyValuePair = { + const config: Record = { key1: 'value1', key2: 'value2', }; @@ -35,7 +32,7 @@ describe('lib/mock-observations/CommonGenerator: ', () => { expect.assertions(1); - expect(commonGenerator.next([])).toStrictEqual({ + expect(commonGenerator.next([])).toEqual({ key1: 'value1', key2: 'value2', }); diff --git a/src/__tests__/unit/builtins/RandIntGenerator.test.ts b/src/__tests__/unit/builtins/RandIntGenerator.test.ts index c904f7350..328f7d6f4 100644 --- a/src/__tests__/unit/builtins/RandIntGenerator.test.ts +++ b/src/__tests__/unit/builtins/RandIntGenerator.test.ts @@ -1,23 +1,20 @@ -import {KeyValuePair} from '../../../types/common'; - -import {ERRORS} from '../../../util/errors'; +import {ERRORS} from '@grnsft/if-core/utils'; import {RandIntGenerator} from '../../../builtins/mock-observations/helpers/rand-int-generator'; -const {InputValidationError} = ERRORS; +import {STRINGS} from '../../../config'; + +const {GlobalConfigError} = ERRORS; +const {INVALID_NAME, MISSING_MIN_MAX, MISSING_GLOBAL_CONFIG} = STRINGS; -describe('lib/mock-observations/RandIntGenerator: ', () => { +describe('builtins/mock-observations/RandIntGenerator: ', () => { describe('initialize', () => { it('throws an error when the generator name is empty string.', async () => { expect.assertions(1); try { RandIntGenerator('', {}); } catch (error) { - expect(error).toEqual( - new InputValidationError( - 'RandIntGenerator: `name` is empty or all spaces.' - ) - ); + expect(error).toEqual(new GlobalConfigError(INVALID_NAME)); } }); @@ -26,11 +23,7 @@ describe('lib/mock-observations/RandIntGenerator: ', () => { try { RandIntGenerator('generator-name', {}); } catch (error) { - expect(error).toEqual( - new InputValidationError( - 'RandIntGenerator: Config must not be null or empty.' - ) - ); + expect(error).toEqual(new GlobalConfigError(MISSING_GLOBAL_CONFIG)); } }); @@ -42,18 +35,14 @@ describe('lib/mock-observations/RandIntGenerator: ', () => { try { RandIntGenerator('random', config); } catch (error) { - expect(error).toEqual( - new InputValidationError( - 'RandIntGenerator: Config is missing min or max.' - ) - ); + expect(error).toEqual(new GlobalConfigError(MISSING_MIN_MAX)); } }); }); describe('next(): ', () => { it('returns a result with valid data.', async () => { - const config: KeyValuePair = { + const config: Record = { min: 10, max: 90, }; diff --git a/src/__tests__/unit/builtins/coefficient.test.ts b/src/__tests__/unit/builtins/coefficient.test.ts index bf7455679..034d20f95 100644 --- a/src/__tests__/unit/builtins/coefficient.test.ts +++ b/src/__tests__/unit/builtins/coefficient.test.ts @@ -1,8 +1,11 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {Coefficient} from '../../../builtins/coefficient'; -import {ERRORS} from '../../../util/errors'; +import {STRINGS} from '../../../config'; -const {InputValidationError, ConfigNotFoundError} = ERRORS; +const {InputValidationError, GlobalConfigError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; describe('builtins/coefficient: ', () => { describe('Coefficient: ', () => { @@ -49,7 +52,6 @@ describe('builtins/coefficient: ', () => { it('throws an error when global config is not provided.', () => { const config = undefined; const coefficient = Coefficient(config!); - const expectedMessage = 'Global config is not provided.'; expect.assertions(1); @@ -62,7 +64,9 @@ describe('builtins/coefficient: ', () => { }, ]); } catch (error) { - expect(error).toStrictEqual(new ConfigNotFoundError(expectedMessage)); + expect(error).toStrictEqual( + new GlobalConfigError(MISSING_GLOBAL_CONFIG) + ); } }); diff --git a/src/__tests__/unit/builtins/copy-param.test.ts b/src/__tests__/unit/builtins/copy-param.test.ts new file mode 100644 index 000000000..64809e44a --- /dev/null +++ b/src/__tests__/unit/builtins/copy-param.test.ts @@ -0,0 +1,124 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + +import {Copy} from '../../../builtins/copy-param'; + +import {STRINGS} from '../../../config'; + +const {GlobalConfigError, InputValidationError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; + +describe('builtins/copy: ', () => { + describe('Copy: ', () => { + const globalConfig = { + 'keep-existing': true, + from: 'original', + to: 'copy', + }; + const copy = Copy(globalConfig); + + describe('init: ', () => { + it('successfully initalized.', () => { + expect(copy).toHaveProperty('metadata'); + expect(copy).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('successfully applies Copy strategy to given input.', () => { + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + original: 'hello', + copy: 'hello', + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = copy.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + original: 'hello', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('throws an error when global config is not provided.', () => { + const config = undefined; + const copy = Copy(config!); + + expect.assertions(1); + + try { + copy.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + original: 1, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new GlobalConfigError(MISSING_GLOBAL_CONFIG) + ); + } + }); + + it('throws an error on missing params in input.', () => { + const globalConfig = { + 'keep-existing': true, + from: 'original', + to: 'copy', + }; + const copy = Copy(globalConfig); + expect.assertions(1); + + try { + copy.execute([ + { + duration: 3600, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError( + '"original" parameter is required. Error code: invalid_type.' + ) + ); + } + }); + it('does not persist the original value when keep-existing==false.', () => { + expect.assertions(1); + const globalConfig = { + 'keep-existing': false, + from: 'original', + to: 'copy', + }; + const copy = Copy(globalConfig); + + const expectedResult = [ + { + duration: 3600, + copy: 'hello', + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = copy.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + original: 'hello', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + }); + }); +}); diff --git a/src/__tests__/unit/builtins/csv-lookup.test.ts b/src/__tests__/unit/builtins/csv-lookup.test.ts index 4e16fd9b6..81e0498b4 100644 --- a/src/__tests__/unit/builtins/csv-lookup.test.ts +++ b/src/__tests__/unit/builtins/csv-lookup.test.ts @@ -2,12 +2,21 @@ jest.mock('fs/promises', () => require('../../../__mocks__/fs')); import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; +import {ERRORS} from '@grnsft/if-core/utils'; import {CSVLookup} from '../../../builtins'; -import {ERRORS} from '../../../util/errors'; +import {STRINGS} from '../../../config'; -const {FileNotFoundError, InputValidationError, ConfigNotFoundError} = ERRORS; +const { + GlobalConfigError, + ReadFileError, + FetchingFileError, + QueryDataNotFoundError, + MissingCSVColumnError, + CSVParseError, +} = ERRORS; +const {MISSING_GLOBAL_CONFIG, MISSING_CSV_COLUMN, NO_QUERY_DATA} = STRINGS; describe('builtins/CSVLookup: ', () => { const mock = new AxiosMockAdapter(axios); @@ -131,7 +140,7 @@ describe('builtins/CSVLookup: ', () => { await csvLookup.execute(input); } catch (error) { if (error instanceof Error) { - expect(error).toBeInstanceOf(FileNotFoundError); + expect(error).toBeInstanceOf(ReadFileError); } } }); @@ -160,7 +169,7 @@ describe('builtins/CSVLookup: ', () => { await csvLookup.execute(input); } catch (error) { if (error instanceof Error) { - expect(error).toBeInstanceOf(FileNotFoundError); + expect(error).toBeInstanceOf(ReadFileError); } } }); @@ -192,7 +201,7 @@ describe('builtins/CSVLookup: ', () => { await csvLookup.execute(input); } catch (error) { if (error instanceof Error) { - expect(error).toBeInstanceOf(FileNotFoundError); + expect(error).toBeInstanceOf(FetchingFileError); } } }); @@ -340,11 +349,8 @@ describe('builtins/CSVLookup: ', () => { await csvLookup.execute(input); } catch (error) { if (error instanceof Error) { - expect(error).toBeInstanceOf(InputValidationError); - expect(error.message).toEqual( - `Error happened while parsing given CSV file: ./file.csv -InputValidationError: One or more of the given query parameters are not found in the target CSV file column headers.` - ); + expect(error).toBeInstanceOf(QueryDataNotFoundError); + expect(error.message).toEqual(NO_QUERY_DATA); } } }); @@ -368,8 +374,8 @@ InputValidationError: One or more of the given query parameters are not found in await csvLookup.execute(input); } catch (error) { if (error instanceof Error) { - expect(error).toBeInstanceOf(ConfigNotFoundError); - expect(error.message).toEqual('Global config is not provided.'); + expect(error).toBeInstanceOf(GlobalConfigError); + expect(error.message).toEqual(MISSING_GLOBAL_CONFIG); } } }); @@ -400,10 +406,9 @@ InputValidationError: One or more of the given query parameters are not found in await csvLookup.execute(input); } catch (error) { if (error instanceof Error) { - expect(error).toBeInstanceOf(InputValidationError); + expect(error).toBeInstanceOf(MissingCSVColumnError); expect(error.message).toEqual( - `Error happened while parsing given CSV file: ./file.csv -InputValidationError: There is no column with name: mock.` + MISSING_CSV_COLUMN(globalConfig.output) ); } } @@ -477,5 +482,33 @@ InputValidationError: There is no column with name: mock.` expect(result).toStrictEqual(expectedResult); }); }); + + it('rejects with CSV parse error', async () => { + process.env.csv = 'fail'; + expect.assertions(1); + const globalConfig = { + filepath: './fail-csv-reader.csv', + query: { + 'cpu-cores-available': 'cpu/available', + 'cpu-cores-utilized': 'cpu/utilized', + 'cpu-manufacturer': 'cpu/manufacturer', + }, + output: [['gpu-count']], + }; + const csvLookup = CSVLookup(globalConfig); + + try { + await csvLookup.execute([ + { + timestamp: '2024-03-01', + 'cpu/available': 16, + 'cpu/utilized': 16, + 'cpu/manufacturer': 'AWS', + }, + ]); + } catch (error) { + expect(error).toBeInstanceOf(CSVParseError); + } + }); }); }); diff --git a/src/__tests__/unit/builtins/divide.test.ts b/src/__tests__/unit/builtins/divide.test.ts index 8f56bb19d..ba75e3879 100644 --- a/src/__tests__/unit/builtins/divide.test.ts +++ b/src/__tests__/unit/builtins/divide.test.ts @@ -1,8 +1,11 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {Divide} from '../../../builtins'; -import {ERRORS} from '../../../util/errors'; +import {STRINGS} from '../../../config'; -const {InputValidationError, ConfigNotFoundError} = ERRORS; +const {InputValidationError, GlobalConfigError, MissingInputDataError} = ERRORS; +const {MISSING_GLOBAL_CONFIG, MISSING_INPUT_DATA} = STRINGS; describe('builtins/divide: ', () => { describe('Divide: ', () => { @@ -103,7 +106,6 @@ describe('builtins/divide: ', () => { }); it('throws an error on missing global config.', async () => { - const expectedMessage = 'Global config is not provided.'; const config = undefined; const divide = Divide(config!); @@ -117,14 +119,13 @@ describe('builtins/divide: ', () => { }, ]); } catch (error) { - expect(error).toStrictEqual(new ConfigNotFoundError(expectedMessage)); + expect(error).toStrictEqual( + new GlobalConfigError(MISSING_GLOBAL_CONFIG) + ); } }); it('throws an error when `denominator` is 0.', async () => { - const expectedMessage = - '"denominator" parameter is number must be greater than 0. Error code: too_small.'; - const globalConfig = { numerator: 'vcpus-allocated', denominator: 0, @@ -134,22 +135,25 @@ describe('builtins/divide: ', () => { expect.assertions(1); - try { - await divide.execute([ - { - timestamp: '2021-01-01T00:00:00Z', - duration: 3600, - 'vcpus-allocated': 24, - }, - ]); - } catch (error) { - expect(error).toStrictEqual(new InputValidationError(expectedMessage)); - } + const response = await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]); + + expect(response).toEqual([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + 'vcpus-allocated-per-second': 24, + }, + ]); }); it('throws an error when `denominator` is string.', async () => { - const expectedMessage = '`10` is missing from the input.'; - const globalConfig = { numerator: 'vcpus-allocated', denominator: '10', @@ -168,7 +172,11 @@ describe('builtins/divide: ', () => { }, ]); } catch (error) { - expect(error).toStrictEqual(new InputValidationError(expectedMessage)); + expect(error).toStrictEqual( + new MissingInputDataError( + MISSING_INPUT_DATA(globalConfig.denominator) + ) + ); } }); }); diff --git a/src/__tests__/unit/builtins/exponent.test.ts b/src/__tests__/unit/builtins/exponent.test.ts index 54c69ffcb..ad438179d 100644 --- a/src/__tests__/unit/builtins/exponent.test.ts +++ b/src/__tests__/unit/builtins/exponent.test.ts @@ -1,10 +1,10 @@ -import {Exponent} from '../../../builtins/exponent'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../../../util/errors'; +import {Exponent} from '../../../builtins/exponent'; const {InputValidationError} = ERRORS; -describe('lib/exponent: ', () => { +describe('builtins/exponent: ', () => { describe('Exponent: ', () => { const globalConfig = { 'input-parameter': 'energy/base', @@ -45,9 +45,6 @@ describe('lib/exponent: ', () => { }); it('throws an error on missing params in input.', async () => { - const expectedMessage = - 'Exponent: energy/base is missing from the input array.'; - expect.assertions(1); try { @@ -59,27 +56,30 @@ describe('lib/exponent: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new InputValidationError( + '"input-parameter" parameter is required. Error code: invalid_type.' + ) ); } }); it('throws an error on input param value not numeric.', async () => { - const expectedMessage = 'Exponent: i-am-not-a-number is not numeric.'; - expect.assertions(1); + const input = [ + { + duration: 3600, + 'energy/base': 'i-am-not-a-number', + timestamp: '2021-01-01T00:00:00Z', + }, + ]; try { - await exponent.execute([ - { - duration: 3600, - 'energy/base': 'i-am-not-a-number', - timestamp: '2021-01-01T00:00:00Z', - }, - ]); + await exponent.execute(input); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new InputValidationError( + '"input-parameter" parameter is expected number, received string. Error code: invalid_type.' + ) ); } }); diff --git a/src/__tests__/unit/builtins/export-csv-raw.test.ts b/src/__tests__/unit/builtins/export-csv-raw.test.ts index 1ebf9e45e..e97242a4d 100644 --- a/src/__tests__/unit/builtins/export-csv-raw.test.ts +++ b/src/__tests__/unit/builtins/export-csv-raw.test.ts @@ -1,12 +1,16 @@ import * as fs from 'fs/promises'; + import {jest} from '@jest/globals'; +import {ERRORS} from '@grnsft/if-core/utils'; import {ExportCSVRaw} from '../../../builtins/export-csv-raw'; -import {ERRORS} from '../../../util/errors'; + +import {STRINGS} from '../../../config'; import {tree, context, outputs} from '../../../__mocks__/builtins/export-csv'; -const {ExhaustError} = ERRORS; +const {ExhaustOutputArgError} = ERRORS; +const {WRITE_CSV_ERROR, OUTPUT_REQUIRED} = STRINGS; jest.mock('fs/promises', () => ({ __esModule: true, @@ -31,7 +35,7 @@ describe('builtins/export-csv-raw: ', () => { it('generates CSV file with correct data.', async () => { const outputPath = 'output#carbon'; const content = - "id,timestamp,cloud/instance-type,region,duration,cpu/utilization,network/energy,energy,cpu/thermal-design-power,grid/carbon-intensity,device/emissions-embodied,time-reserved,device/expected-lifespan,resources-reserved,resources-total,functional-unit-time,cpu/energy,carbon-plus-energy',carbon-embodied,carbon-operational,carbon,sci\nchildren.child-1.outputs.0,2023-12-12T00:00:00.000Z,A1,uk-west,1,10,10,5,100,800,1533.12,3600,94608000,1,8,1 min,0.000008888888888888888,10.000008888888889,0.0000020256215119228817,4000,4000.0000020256216,240000.0001215373\nchildren.child-2.outputs.0,2023-12-12T00:00:00.000Z,A1,uk-west,1,30,10,5,100,800,1533.12,3600,94608000,1,8,1 min,0.00001650338753387534,10.000016503387533,0.0000020256215119228817,4000,4000.0000020256216,240000.0001215373\noutputs.0,2023-12-12T00:00:00.000Z,,,1,,,,,,,,,,,,,,,,8000.000004051243,"; + "id,timestamp,cloud/instance-type,region,duration,cpu/utilization,network/energy,energy,cpu/thermal-design-power,grid/carbon-intensity,device/emissions-embodied,time-reserved,device/expected-lifespan,resources-reserved,resources-total,cpu/energy,carbon-plus-energy',carbon-embodied,carbon-operational,carbon,sci\nchildren.child-1.outputs.0,2023-12-12T00:00:00.000Z,A1,uk-west,1,10,10,5,100,800,1533.12,3600,94608000,1,8,0.000008888888888888888,10.000008888888889,0.0000020256215119228817,4000,4000.0000020256216,240000.0001215373\nchildren.child-2.outputs.0,2023-12-12T00:00:00.000Z,A1,uk-west,1,30,10,5,100,800,1533.12,3600,94608000,1,8,0.00001650338753387534,10.000016503387533,0.0000020256215119228817,4000,4000.0000020256216,240000.0001215373\noutputs.0,2023-12-12T00:00:00.000Z,,,1,,,,,,,,,,,,,,,8000.000004051243,"; await exportCSVRaw.execute(tree, context, outputPath); @@ -40,18 +44,17 @@ describe('builtins/export-csv-raw: ', () => { it('throws an error when the CSV file could not be created.', async () => { const outputPath = 'output#carbon'; + const expectedMessage = 'Could not write CSV file.'; expect.assertions(1); - jest - .spyOn(fs, 'writeFile') - .mockRejectedValue('Could not write CSV file.'); + jest.spyOn(fs, 'writeFile').mockRejectedValue(expectedMessage); await expect( exportCSVRaw.execute(tree, context, outputPath) ).rejects.toThrow( - new ExhaustError( - 'Failed to write CSV to output#carbon: Could not write CSV file.' + new ExhaustOutputArgError( + WRITE_CSV_ERROR(outputPath, expectedMessage) ) ); }); @@ -65,8 +68,8 @@ describe('builtins/export-csv-raw: ', () => { try { await exportCSVRaw.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); - expect(error).toEqual(new ExhaustError('Output path is required.')); + expect(error).toBeInstanceOf(ExhaustOutputArgError); + expect(error).toEqual(new ExhaustOutputArgError(OUTPUT_REQUIRED)); } }); }); diff --git a/src/__tests__/unit/builtins/export-csv.test.ts b/src/__tests__/unit/builtins/export-csv.test.ts index 94414a970..78feaf534 100644 --- a/src/__tests__/unit/builtins/export-csv.test.ts +++ b/src/__tests__/unit/builtins/export-csv.test.ts @@ -1,9 +1,12 @@ import * as fs from 'fs/promises'; + import {stringify} from 'csv-stringify/sync'; import {jest} from '@jest/globals'; +import {ERRORS} from '@grnsft/if-core/utils'; import {ExportCSV} from '../../../builtins/export-csv'; -import {ERRORS} from '../../../util/errors'; + +import {STRINGS} from '../../../config'; import { tree, @@ -13,7 +16,8 @@ import { aggregation, } from '../../../__mocks__/builtins/export-csv'; -const {ExhaustError} = ERRORS; +const {ExhaustOutputArgError} = ERRORS; +const {OUTPUT_REQUIRED, CSV_EXPORT} = STRINGS; jest.mock('fs/promises', () => ({ writeFile: jest.fn<() => Promise>().mockResolvedValue(), @@ -197,8 +201,8 @@ describe('builtins/export-csv: ', () => { try { await exportCSV.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); - expect(error).toEqual(new ExhaustError('Output path is required.')); + expect(error).toBeInstanceOf(ExhaustOutputArgError); + expect(error).toEqual(new ExhaustOutputArgError(OUTPUT_REQUIRED)); } }); @@ -210,10 +214,8 @@ describe('builtins/export-csv: ', () => { try { await exportCSV.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); - expect(error).toEqual( - new ExhaustError('Output path should contain `#`.') - ); + expect(error).toBeInstanceOf(ExhaustOutputArgError); + expect(error).toEqual(new ExhaustOutputArgError(CSV_EXPORT)); } }); @@ -225,12 +227,8 @@ describe('builtins/export-csv: ', () => { try { await exportCSV.execute(tree, context, outputPath); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); - expect(error).toEqual( - new ExhaustError( - 'CSV export criteria is not found in output path. Please append it after --output #.' - ) - ); + expect(error).toBeInstanceOf(ExhaustOutputArgError); + expect(error).toEqual(new ExhaustOutputArgError(CSV_EXPORT)); } }); }); diff --git a/src/__tests__/unit/builtins/export-yaml.test.ts b/src/__tests__/unit/builtins/export-yaml.test.ts index a2157e4ef..fb7954c1e 100644 --- a/src/__tests__/unit/builtins/export-yaml.test.ts +++ b/src/__tests__/unit/builtins/export-yaml.test.ts @@ -1,14 +1,18 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {ExportYaml} from '../../../builtins/export-yaml'; -import {ERRORS} from '../../../util/errors'; import {saveYamlFileAs} from '../../../util/yaml'; +import {STRINGS} from '../../../config'; + import {tree, context} from '../../../__mocks__/builtins/export-csv'; jest.mock('../../../util/yaml', () => ({ saveYamlFileAs: jest.fn(), })); -const {ExhaustError} = ERRORS; +const {ExhaustOutputArgError} = ERRORS; +const {OUTPUT_REQUIRED} = STRINGS; describe('builtins/export-yaml: ', () => { describe('ExportYaml: ', () => { @@ -38,8 +42,8 @@ describe('builtins/export-yaml: ', () => { try { await exportYaml.execute({}, context, ''); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); - expect(error).toEqual(new ExhaustError('Output path is required.')); + expect(error).toBeInstanceOf(ExhaustOutputArgError); + expect(error).toEqual(new ExhaustOutputArgError(OUTPUT_REQUIRED)); } }); }); diff --git a/src/__tests__/unit/builtins/group-by.test.ts b/src/__tests__/unit/builtins/group-by.test.ts index 9c99ee700..3d676148b 100644 --- a/src/__tests__/unit/builtins/group-by.test.ts +++ b/src/__tests__/unit/builtins/group-by.test.ts @@ -1,7 +1,11 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {GroupBy} from '../../../builtins/group-by'; -import {ERRORS} from '../../../util/errors'; -const {InvalidGroupingError, InputValidationError} = ERRORS; +import {STRINGS} from '../../../config'; + +const {InvalidGroupingError, InputValidationError, GlobalConfigError} = ERRORS; +const {MISSING_GLOBAL_CONFIG, INVALID_GROUP_BY} = STRINGS; describe('builtins/group-by: ', () => { describe('GroupBy: ', () => { @@ -92,10 +96,8 @@ describe('builtins/group-by: ', () => { try { plugin.execute(inputs, config!); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); - expect(error).toEqual( - new InputValidationError('Config is not provided.') - ); + expect(error).toBeInstanceOf(GlobalConfigError); + expect(error).toEqual(new GlobalConfigError(MISSING_GLOBAL_CONFIG)); } }); @@ -166,7 +168,7 @@ describe('builtins/group-by: ', () => { } catch (error) { expect(error).toBeInstanceOf(InvalidGroupingError); expect(error).toEqual( - new InvalidGroupingError('Invalid group unknown.') + new InvalidGroupingError(INVALID_GROUP_BY(config.group[2])) ); } }); diff --git a/src/__tests__/unit/builtins/interpolation.test.ts b/src/__tests__/unit/builtins/interpolation.test.ts index c4bba939f..6219e01ec 100644 --- a/src/__tests__/unit/builtins/interpolation.test.ts +++ b/src/__tests__/unit/builtins/interpolation.test.ts @@ -1,8 +1,17 @@ +import {ERRORS} from '@grnsft/if-core/utils'; +import {Method} from '@grnsft/if-core/types'; + import {Interpolation} from '../../../builtins'; -import {Method} from '../../../builtins/interpolation/types'; -import {ERRORS} from '../../../util/errors'; -const {InputValidationError, ConfigNotFoundError} = ERRORS; +import {STRINGS} from '../../../config'; + +const {InputValidationError, GlobalConfigError} = ERRORS; +const { + MISSING_GLOBAL_CONFIG, + WITHIN_THE_RANGE, + ARRAY_LENGTH_NON_EMPTY, + X_Y_EQUAL, +} = STRINGS; describe('builtins/interpolation: ', () => { describe('Interpolation: ', () => { @@ -144,10 +153,8 @@ describe('builtins/interpolation: ', () => { try { plugin.execute(inputs); } catch (error) { - expect(error).toBeInstanceOf(ConfigNotFoundError); - expect(error).toEqual( - new ConfigNotFoundError('Global config is not provided.') - ); + expect(error).toBeInstanceOf(GlobalConfigError); + expect(error).toEqual(new GlobalConfigError(MISSING_GLOBAL_CONFIG)); } }); @@ -163,11 +170,7 @@ describe('builtins/interpolation: ', () => { plugin.execute(inputs); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); - expect(error).toEqual( - new InputValidationError( - 'The length of `x` and `y` should be equal' - ) - ); + expect(error).toEqual(new InputValidationError(X_Y_EQUAL)); } }); @@ -184,11 +187,7 @@ describe('builtins/interpolation: ', () => { plugin.execute(inputs); } catch (error) { expect(error).toBeInstanceOf(InputValidationError); - expect(error).toEqual( - new InputValidationError( - 'The target x value must be within the range of the given x values' - ) - ); + expect(error).toEqual(new InputValidationError(WITHIN_THE_RANGE)); } }); it('throws an error when the the length of the input arrays is <2', () => { @@ -213,9 +212,7 @@ describe('builtins/interpolation: ', () => { } catch (error) { expect(error).toBeInstanceOf(InputValidationError); expect(error).toEqual( - new InputValidationError( - 'the length of the input arrays must be greater than 1' - ) + new InputValidationError(ARRAY_LENGTH_NON_EMPTY) ); } }); diff --git a/src/__tests__/unit/builtins/mock-observations.test.ts b/src/__tests__/unit/builtins/mock-observations.test.ts index 37d3a04b4..fd28d5595 100644 --- a/src/__tests__/unit/builtins/mock-observations.test.ts +++ b/src/__tests__/unit/builtins/mock-observations.test.ts @@ -1,10 +1,13 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {MockObservations} from '../../../builtins/mock-observations'; -import {ERRORS} from '../../../util/errors'; +import {STRINGS} from '../../../config'; -const {InputValidationError} = ERRORS; +const {InputValidationError, GlobalConfigError} = ERRORS; +const {INVALID_MIN_MAX} = STRINGS; -describe('lib/mock-observations: ', () => { +describe('builtins/mock-observations: ', () => { describe('init: ', () => { it('successfully initalized.', () => { const mockObservations = MockObservations({ @@ -88,8 +91,6 @@ describe('lib/mock-observations: ', () => { }); it('throws an error when the `min` is greater then `max` of `randint` config.', async () => { - const errorMessage = - 'RandIntGenerator: Min value should not be greater than or equal to max value of cpu/utilization.'; const config = { 'timestamp-from': '2023-07-06T00:00', 'timestamp-to': '2023-07-06T00:01', @@ -112,8 +113,10 @@ describe('lib/mock-observations: ', () => { try { await mockObservations.execute([]); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); - expect(error).toEqual(new InputValidationError(errorMessage)); + expect(error).toBeInstanceOf(GlobalConfigError); + expect(error).toEqual( + new GlobalConfigError(INVALID_MIN_MAX('cpu/utilization')) + ); } }); diff --git a/src/__tests__/unit/builtins/multiply.test.ts b/src/__tests__/unit/builtins/multiply.test.ts index a4451179d..d15a7e8f9 100644 --- a/src/__tests__/unit/builtins/multiply.test.ts +++ b/src/__tests__/unit/builtins/multiply.test.ts @@ -1,10 +1,10 @@ -import {Multiply} from '../../../builtins/multiply'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../../../util/errors'; +import {Multiply} from '../../../builtins/multiply'; const {InputValidationError} = ERRORS; -describe('lib/multiply: ', () => { +describe('builtins/multiply: ', () => { describe('Multiply: ', () => { const globalConfig = { 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], @@ -48,9 +48,6 @@ describe('lib/multiply: ', () => { }); it('throws an error on missing params in input.', async () => { - const expectedMessage = - 'Multiply: cpu/energy is missing from the input array.'; - expect.assertions(1); try { @@ -62,7 +59,9 @@ describe('lib/multiply: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new InputValidationError( + '"cpu/energy" parameter is required. Error code: invalid_type.,"network/energy" parameter is required. Error code: invalid_type.,"memory/energy" parameter is required. Error code: invalid_type.' + ) ); } }); @@ -88,10 +87,10 @@ describe('lib/multiply: ', () => { const expectedResult = [ { duration: 3600, + timestamp: '2021-01-01T00:00:00Z', carbon: 3, 'other-carbon': 2, 'carbon-product': 6, - timestamp: '2021-01-01T00:00:00Z', }, ]; diff --git a/src/__tests__/unit/builtins/regex.test.ts b/src/__tests__/unit/builtins/regex.test.ts index 19ccdf89d..54a70ed9e 100644 --- a/src/__tests__/unit/builtins/regex.test.ts +++ b/src/__tests__/unit/builtins/regex.test.ts @@ -1,10 +1,13 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {Regex} from '../../../builtins/regex'; -import {ERRORS} from '../../../util/errors'; +import {STRINGS} from '../../../config'; -const {InputValidationError, ConfigValidationError} = ERRORS; +const {GlobalConfigError, MissingInputDataError, RegexMismatchError} = ERRORS; +const {MISSING_GLOBAL_CONFIG, MISSING_INPUT_DATA, REGEX_MISMATCH} = STRINGS; -describe('lib/regex: ', () => { +describe('builtins/regex: ', () => { describe('Regex: ', () => { const globalConfig = { parameter: 'physical-processor', @@ -81,7 +84,6 @@ describe('lib/regex: ', () => { it('throws an error when `parameter` does not match to `match`.', async () => { const physicalProcessor = 'Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz'; - const expectedMessage = `Regex: \`${physicalProcessor}\` does not match the /^(^:)+/ regex expression.`; const globalConfig = { parameter: 'physical-processor', @@ -102,14 +104,14 @@ describe('lib/regex: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new RegexMismatchError( + REGEX_MISMATCH(physicalProcessor, '/^(^:)+/') + ) ); } }); it('throws an error on missing global config.', async () => { - const expectedMessage = 'Regex: Configuration data is missing.'; - const config = undefined; const regex = Regex(config!); @@ -124,15 +126,12 @@ describe('lib/regex: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new ConfigValidationError(expectedMessage) + new GlobalConfigError(MISSING_GLOBAL_CONFIG) ); } }); it('throws an error on missing params in input.', async () => { - const expectedMessage = - 'Regex: `physical-processor` is missing from the input.'; - expect.assertions(1); try { @@ -144,7 +143,7 @@ describe('lib/regex: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new MissingInputDataError(MISSING_INPUT_DATA('physical-processor')) ); } }); diff --git a/src/__tests__/unit/builtins/sci-embodied.test.ts b/src/__tests__/unit/builtins/sci-embodied.test.ts index 75f9f12fd..a3f5bcbff 100644 --- a/src/__tests__/unit/builtins/sci-embodied.test.ts +++ b/src/__tests__/unit/builtins/sci-embodied.test.ts @@ -1,9 +1,13 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {SciEmbodied} from '../../../builtins/sci-embodied'; -import {ERRORS} from '../../../util/errors'; + +import {STRINGS} from '../../../config'; const {InputValidationError} = ERRORS; +const {SCI_EMBODIED_ERROR} = STRINGS; -describe('lib/sci-embodied:', () => { +describe('builtins/sci-embodied:', () => { describe('SciEmbodied: ', () => { const sciEmbodied = SciEmbodied(); @@ -191,8 +195,6 @@ describe('lib/sci-embodied:', () => { }); it('throws an error when `device/emissions-embodied` is string.', async () => { - const errorMessage = - '"device/emissions-embodied" parameter is not a valid number in input. please provide it as `gco2e`. Error code: invalid_union.'; const inputs = [ { timestamp: '2021-01-01T00:00:00Z', @@ -216,7 +218,13 @@ describe('lib/sci-embodied:', () => { try { await sciEmbodied.execute(inputs); } catch (error) { - expect(error).toStrictEqual(new InputValidationError(errorMessage)); + expect(error).toStrictEqual( + new InputValidationError( + `"device/emissions-embodied" parameter is ${SCI_EMBODIED_ERROR( + 'gco2e' + )}. Error code: invalid_union.` + ) + ); expect(error).toBeInstanceOf(InputValidationError); } }); @@ -268,8 +276,6 @@ describe('lib/sci-embodied:', () => { }); it('throws an exception on invalid values.', async () => { - const errorMessage = - '"device/emissions-embodied" parameter is not a valid number in input. please provide it as `gco2e`. Error code: invalid_union.'; const inputs = [ { timestamp: '2021-01-01T00:00:00Z', @@ -285,8 +291,14 @@ describe('lib/sci-embodied:', () => { try { await sciEmbodied.execute(inputs); } catch (error) { - expect(error).toStrictEqual(new InputValidationError(errorMessage)); expect(error).toBeInstanceOf(InputValidationError); + expect(error).toStrictEqual( + new InputValidationError( + `"device/emissions-embodied" parameter is ${SCI_EMBODIED_ERROR( + 'gco2e' + )}. Error code: invalid_union.` + ) + ); } }); }); diff --git a/src/__tests__/unit/builtins/sci.test.ts b/src/__tests__/unit/builtins/sci.test.ts index 2b6261cda..6a8ddc8db 100644 --- a/src/__tests__/unit/builtins/sci.test.ts +++ b/src/__tests__/unit/builtins/sci.test.ts @@ -1,10 +1,10 @@ -import {Sci} from '../../../builtins/sci'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../../../util/errors'; +import {Sci} from '../../../builtins/sci'; -const {InputValidationError} = ERRORS; +const {MissingInputDataError} = ERRORS; -describe('lib/sci:', () => { +describe('builtins/sci:', () => { describe('Sci: ', () => { const sci = Sci({'functional-unit': 'users'}); @@ -113,7 +113,7 @@ describe('lib/sci:', () => { try { await sci.execute(inputs); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); + expect(error).toBeInstanceOf(MissingInputDataError); } }); @@ -136,9 +136,30 @@ describe('lib/sci:', () => { try { await sci.execute(inputs); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); + expect(error).toBeInstanceOf(MissingInputDataError); } }); }); + + it('fallbacks to carbon value, if functional unit is 0.', async () => { + const sci = Sci({ + 'functional-unit': 'requests', + }); + const inputs = [ + { + timestamp: '2021-01-01T00:00:00Z', + 'carbon-operational': 0.2, + 'carbon-embodied': 0.05, + carbon: 0.205, + duration: 1, + requests: 0, + }, + ]; + const result = await sci.execute(inputs); + + expect.assertions(1); + + expect(result).toStrictEqual([{...inputs[0], sci: inputs[0].carbon}]); + }); }); }); diff --git a/src/__tests__/unit/builtins/shell.test.ts b/src/__tests__/unit/builtins/shell.test.ts index 7f1ffd58c..96de944f1 100644 --- a/src/__tests__/unit/builtins/shell.test.ts +++ b/src/__tests__/unit/builtins/shell.test.ts @@ -1,16 +1,15 @@ import {spawnSync} from 'child_process'; import {loadAll} from 'js-yaml'; +import {ERRORS} from '@grnsft/if-core/utils'; import {Shell} from '../../../builtins/shell'; -import {ERRORS} from '../../../util/errors'; - -const {InputValidationError} = ERRORS; +const {InputValidationError, ProcessExecutionError} = ERRORS; jest.mock('child_process'); jest.mock('js-yaml'); -describe('lib/shell', () => { +describe('builtins/shell', () => { describe('Shell', () => { const shell = Shell({}); @@ -87,9 +86,9 @@ describe('lib/shell', () => { try { await shell.execute(inputs); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); + expect(error).toBeInstanceOf(ProcessExecutionError); expect(error).toStrictEqual( - new InputValidationError('Could not run the command') + new ProcessExecutionError('Could not run the command') ); } }); diff --git a/src/__tests__/unit/builtins/subtract.test.ts b/src/__tests__/unit/builtins/subtract.test.ts index 7ac717142..fd4cf7c96 100644 --- a/src/__tests__/unit/builtins/subtract.test.ts +++ b/src/__tests__/unit/builtins/subtract.test.ts @@ -1,10 +1,10 @@ -import {Subtract} from '../../../builtins/subtract'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../../../util/errors'; +import {Subtract} from '../../../builtins/subtract'; const {InputValidationError} = ERRORS; -describe('lib/subtract: ', () => { +describe('builtins/subtract: ', () => { describe('Subtract: ', () => { const globalConfig = { 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], @@ -48,9 +48,6 @@ describe('lib/subtract: ', () => { }); it('throws an error on missing params in input.', async () => { - const expectedMessage = - 'Subtract: cpu/energy is missing from the input array.'; - expect.assertions(1); try { @@ -62,7 +59,9 @@ describe('lib/subtract: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new InputValidationError( + '"cpu/energy" parameter is required. Error code: invalid_type.,"network/energy" parameter is required. Error code: invalid_type.,"memory/energy" parameter is required. Error code: invalid_type.' + ) ); } }); diff --git a/src/__tests__/unit/builtins/sum.test.ts b/src/__tests__/unit/builtins/sum.test.ts index 734ea8db2..1810fc13a 100644 --- a/src/__tests__/unit/builtins/sum.test.ts +++ b/src/__tests__/unit/builtins/sum.test.ts @@ -1,10 +1,13 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {Sum} from '../../../builtins/sum'; -import {ERRORS} from '../../../util/errors'; +import {STRINGS} from '../../../config'; -const {InputValidationError, ConfigNotFoundError} = ERRORS; +const {GlobalConfigError, InputValidationError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; -describe('lib/sum: ', () => { +describe('builtins/sum: ', () => { describe('Sum: ', () => { const globalConfig = { 'input-parameters': ['cpu/energy', 'network/energy', 'memory/energy'], @@ -48,7 +51,6 @@ describe('lib/sum: ', () => { }); it('throws an error when global config is not provided.', () => { - const expectedMessage = 'Global config is not provided.'; const config = undefined; const sum = Sum(config!); @@ -65,13 +67,13 @@ describe('lib/sum: ', () => { }, ]); } catch (error) { - expect(error).toStrictEqual(new ConfigNotFoundError(expectedMessage)); + expect(error).toStrictEqual( + new GlobalConfigError(MISSING_GLOBAL_CONFIG) + ); } }); it('throws an error on missing params in input.', () => { - const expectedMessage = 'cpu/energy is missing from the input array.'; - expect.assertions(1); try { @@ -83,7 +85,9 @@ describe('lib/sum: ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(expectedMessage) + new InputValidationError( + '"cpu/energy" parameter is required. Error code: invalid_type.,"network/energy" parameter is required. Error code: invalid_type.,"memory/energy" parameter is required. Error code: invalid_type.' + ) ); } }); diff --git a/src/__tests__/unit/builtins/time-sync.test.ts b/src/__tests__/unit/builtins/time-sync.test.ts index 4839ad6ca..6dd984659 100644 --- a/src/__tests__/unit/builtins/time-sync.test.ts +++ b/src/__tests__/unit/builtins/time-sync.test.ts @@ -1,11 +1,25 @@ -import {TimeSync} from '../../../builtins/time-sync'; -import {ERRORS} from '../../../util/errors'; +import {ERRORS} from '@grnsft/if-core/utils'; import {Settings, DateTime} from 'luxon'; + +import {TimeSync} from '../../../builtins/time-sync'; + import {STRINGS} from '../../../config'; -Settings.defaultZone = 'utc'; -const {InputValidationError} = ERRORS; -const {INVALID_OBSERVATION_OVERLAP, INVALID_TIME_NORMALIZATION} = STRINGS; +Settings.defaultZone = 'utc'; +const { + InputValidationError, + InvalidPaddingError, + InvalidDateInInputError, + InvalidInputError, + GlobalConfigError, +} = ERRORS; + +const { + INVALID_OBSERVATION_OVERLAP, + INVALID_TIME_NORMALIZATION, + AVOIDING_PADDING_BY_EDGES, + INVALID_DATE_TYPE, +} = STRINGS; jest.mock('luxon', () => { const originalModule = jest.requireActual('luxon'); @@ -196,7 +210,7 @@ describe('execute(): ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(INVALID_TIME_NORMALIZATION) + new GlobalConfigError(INVALID_TIME_NORMALIZATION) ); } }); @@ -228,7 +242,7 @@ describe('execute(): ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(INVALID_OBSERVATION_OVERLAP) + new InvalidInputError(INVALID_OBSERVATION_OVERLAP) ); } }); @@ -258,7 +272,7 @@ describe('execute(): ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError(INVALID_OBSERVATION_OVERLAP) + new InvalidInputError(INVALID_OBSERVATION_OVERLAP) ); } }); @@ -335,22 +349,23 @@ describe('execute(): ', () => { interval: 10, 'allow-padding': true, }; + const data = [ + { + timestamp: 45, + duration: 10, + 'cpu/utilization': 10, + }, + ]; const timeModel = TimeSync(basicConfig); expect.assertions(2); try { - await timeModel.execute([ - { - timestamp: 45, - duration: 10, - 'cpu/utilization': 10, - }, - ]); + await timeModel.execute(data); } catch (error) { - expect(error).toBeInstanceOf(InputValidationError); + expect(error).toBeInstanceOf(InvalidDateInInputError); expect(error).toStrictEqual( - new InputValidationError('Unexpected date datatype: number: 45') + new InvalidDateInInputError(INVALID_DATE_TYPE(data[0].timestamp)) ); } }); @@ -675,7 +690,7 @@ describe('execute(): ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError('Avoiding padding at start') + new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(true, false)) ); } }); @@ -735,7 +750,7 @@ describe('execute(): ', () => { ]); } catch (error) { expect(error).toStrictEqual( - new InputValidationError('Avoiding padding at start and end') + new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(true, true)) ); } }); diff --git a/src/__tests__/unit/config/strings.test.ts b/src/__tests__/unit/config/strings.test.ts deleted file mode 100644 index bdc1f871f..000000000 --- a/src/__tests__/unit/config/strings.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import {STRINGS} from '../../../config/strings'; - -const { - NOT_NATIVE_PLUGIN, - INVALID_MODULE_PATH, - INVALID_AGGREGATION_METHOD, - METRIC_MISSING, - AVOIDING_PADDING, - AVOIDING_PADDING_BY_EDGES, - INVALID_GROUP_BY, - REJECTING_OVERRIDE, - INVALID_EXHAUST_PLUGIN, - UNKNOWN_PARAM, - NOT_INITALIZED_PLUGIN, -} = STRINGS; - -describe('config/strings: ', () => { - describe('NOT_NATIVE_PLUGIN(): ', () => { - it('successfully injects path into message.', () => { - const path = 'mock/path'; - const expectedMessage = ` -You are using plugin ${path} which is not part of the Impact Framework standard library. You should do your own research to ensure the plugins are up to date and accurate. They may not be actively maintained.`; - - expect(NOT_NATIVE_PLUGIN(path)).toEqual(expectedMessage); - }); - }); - - describe('INVALID_MODULE_PATH(): ', () => { - it('successfully appends given param to message.', () => { - const param = 'mock-param'; - - const expectedMessage = `Provided module: '${param}' is invalid or not found.`; - - expect(INVALID_MODULE_PATH(param)).toEqual(expectedMessage); - }); - }); - - describe('AVOIDING_PADDING(): ', () => { - it('successfully appends given param to message.', () => { - const param = 'mock-param'; - - const expectedMessage = `Avoiding padding at ${param}`; - - expect(AVOIDING_PADDING(param)).toEqual(expectedMessage); - }); - }); - - describe('AVOIDING_PADDING_BY_EDGES(): ', () => { - it('successfully appends given start and end params.', () => { - const start = true; - const end = true; - - const expectedMessage = 'Avoiding padding at start and end'; - - expect(AVOIDING_PADDING_BY_EDGES(start, end)).toEqual(expectedMessage); - }); - - it('successfully appends given start param.', () => { - const start = true; - const end = false; - - const expectedMessage = 'Avoiding padding at start'; - - expect(AVOIDING_PADDING_BY_EDGES(start, end)).toEqual(expectedMessage); - }); - - it('successfully appends given end param.', () => { - const start = false; - const end = true; - - const expectedMessage = 'Avoiding padding at end'; - - expect(AVOIDING_PADDING_BY_EDGES(start, end)).toEqual(expectedMessage); - }); - }); - - describe('INVALID_AGGREGATION_METHOD(): ', () => { - it('successfully appends given param to message.', () => { - const param = 'mock-param'; - - const expectedMessage = `Aggregation is not possible for given ${param} since method is 'none'.`; - - expect(INVALID_AGGREGATION_METHOD(param)).toEqual(expectedMessage); - }); - }); - - describe('METRIC_MISSING(): ', () => { - it('successfully appends given param to message.', () => { - const metric = 'mock-metric'; - const index = 0; - - const expectedMessage = `Aggregation metric ${metric} is not found in inputs[${index}].`; - - expect(METRIC_MISSING(metric, index)).toEqual(expectedMessage); - }); - }); - - describe('INVALID_GROUP_BY(): ', () => { - it('injects type in given message.', () => { - const type = 'mock-type'; - const message = `Invalid group ${type}.`; - - expect(INVALID_GROUP_BY(type)).toEqual(message); - }); - }); - - describe('REJECTING_OVERRIDE(): ', () => { - it('inejcts param name into message.', () => { - const param: any = { - name: 'mock-name', - description: 'mock-description', - aggregation: 'sum', - unit: 'mock-unit', - }; - - expect(REJECTING_OVERRIDE(param)); - }); - }); - - describe('INVALID_EXHAUST_PLUGIN(): ', () => { - it('injects plugin name into message.', () => { - const pluginName = 'mock-plugin'; - const message = `Invalid exhaust plugin: ${pluginName}.`; - - expect(INVALID_EXHAUST_PLUGIN(pluginName)).toEqual(message); - }); - }); - - describe('AVOIDING_PADDING(): ', () => { - it('successfully appends given param to message.', () => { - const description_suffix = 'pad description'; - - const expectedMessage = `Avoiding padding at ${description_suffix}`; - - expect(AVOIDING_PADDING(description_suffix)).toEqual(expectedMessage); - }); - }); - - describe('UNKNOWN_PARAM(): ', () => { - it('injects name into message.', () => { - const name = 'mock-name'; - const message = `Unknown parameter: ${name}. Using 'sum' aggregation method.`; - - expect(UNKNOWN_PARAM(name)).toEqual(message); - }); - }); - - describe('NOT_INITALIZED_PLUGIN(): ', () => { - it('injects name into message.', () => { - const name = 'mock-name'; - const message = `Not initalized plugin: ${name}. Check if ${name} is in 'manifest.initalize.plugins'.`; - - expect(NOT_INITALIZED_PLUGIN(name)).toEqual(message); - }); - }); - - describe('AVOIDING_PADDING_BY_EDGES(): ', () => { - it('successfully combines boolean params into a description.', () => { - let description_suffix = 'start and end'; - let expectedMessage = `Avoiding padding at ${description_suffix}`; - - expect(AVOIDING_PADDING_BY_EDGES(true, true)).toEqual(expectedMessage); - description_suffix = 'start'; - expectedMessage = `Avoiding padding at ${description_suffix}`; - expect(AVOIDING_PADDING_BY_EDGES(true, false)).toEqual(expectedMessage); - description_suffix = 'end'; - expectedMessage = `Avoiding padding at ${description_suffix}`; - expect(AVOIDING_PADDING_BY_EDGES(false, true)).toEqual(expectedMessage); - }); - }); -}); diff --git a/src/__tests__/unit/lib/exhaust.test.ts b/src/__tests__/unit/lib/exhaust.test.ts index 97db0b9dc..49db14370 100644 --- a/src/__tests__/unit/lib/exhaust.test.ts +++ b/src/__tests__/unit/lib/exhaust.test.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ jest.mock('fs', () => require('../../../__mocks__/fs')); -import {exhaust} from '../../../lib/exhaust'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../../../util/errors'; +import {exhaust} from '../../../lib/exhaust'; import {STRINGS} from '../../../config'; -const {ExhaustError} = ERRORS; -const {INVALID_EXHAUST_PLUGIN} = STRINGS; +const {ExhaustOutputArgError, InvalidExhaustPluginError} = ERRORS; +const {INVALID_EXHAUST_PLUGIN, OUTPUT_REQUIRED} = STRINGS; describe('lib/exhaust: ', () => { describe('exhaust(): ', () => { @@ -50,7 +50,6 @@ describe('lib/exhaust: ', () => { outputs: ['yaml'], }, }; - const expectedMessage = 'Output path is required.'; expect.assertions(2); @@ -58,10 +57,10 @@ describe('lib/exhaust: ', () => { // @ts-ignore await exhaust(tree, context, {}); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); + expect(error).toBeInstanceOf(ExhaustOutputArgError); - if (error instanceof ExhaustError) { - expect(error.message).toEqual(expectedMessage); + if (error instanceof ExhaustOutputArgError) { + expect(error.message).toEqual(OUTPUT_REQUIRED); } } }); @@ -73,9 +72,6 @@ describe('lib/exhaust: ', () => { outputs: ['mock'], }, }; - const expectedMessage = INVALID_EXHAUST_PLUGIN( - context.initialize.outputs[0] - ); expect.assertions(2); @@ -83,10 +79,12 @@ describe('lib/exhaust: ', () => { // @ts-ignore await exhaust(tree, context, {}); } catch (error) { - expect(error).toBeInstanceOf(ExhaustError); + expect(error).toBeInstanceOf(InvalidExhaustPluginError); - if (error instanceof ExhaustError) { - expect(error.message).toEqual(expectedMessage); + if (error instanceof InvalidExhaustPluginError) { + expect(error.message).toEqual( + INVALID_EXHAUST_PLUGIN(context.initialize.outputs[0]) + ); } } }); diff --git a/src/__tests__/unit/lib/initialize.test.ts b/src/__tests__/unit/lib/initialize.test.ts index ad5541684..ff3c10c45 100644 --- a/src/__tests__/unit/lib/initialize.test.ts +++ b/src/__tests__/unit/lib/initialize.test.ts @@ -10,15 +10,19 @@ jest.mock('../../../util/log-memoize', () => ({ memoizedLog: mockLog, })); -import {initialize} from '../../../lib/initialize'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../../../util/errors'; +import {initialize} from '../../../lib/initialize'; import {STRINGS} from '../../../config'; import {GlobalPlugins} from '../../../types/manifest'; -const {PluginCredentialError, ModuleInitializationError} = ERRORS; +const { + MissingPluginPathError, + MissingPluginMethodError, + PluginInitializationError, +} = ERRORS; const {MISSING_METHOD, MISSING_PATH, INVALID_MODULE_PATH} = STRINGS; describe('lib/initalize: ', () => { @@ -81,9 +85,9 @@ describe('lib/initalize: ', () => { try { await initialize(plugins); } catch (error) { - expect(error).toBeInstanceOf(PluginCredentialError); + expect(error).toBeInstanceOf(MissingPluginPathError); - if (error instanceof PluginCredentialError) { + if (error instanceof MissingPluginPathError) { expect(error.message).toEqual(MISSING_PATH); } } @@ -103,9 +107,9 @@ describe('lib/initalize: ', () => { try { await initialize(plugins); } catch (error) { - expect(error).toBeInstanceOf(PluginCredentialError); + expect(error).toBeInstanceOf(MissingPluginMethodError); - if (error instanceof PluginCredentialError) { + if (error instanceof MissingPluginMethodError) { expect(error.message).toEqual(MISSING_METHOD); } } @@ -160,14 +164,16 @@ describe('lib/initalize: ', () => { try { await initialize(plugins); - } catch (error) { - expect(error).toBeInstanceOf(ModuleInitializationError); - - if (error instanceof ModuleInitializationError) { - expect(error.message).toEqual( - INVALID_MODULE_PATH(plugins.mockavizta.path) - ); - } + } catch (error: any) { + expect(error).toBeInstanceOf(PluginInitializationError); + expect(error.message).toEqual( + INVALID_MODULE_PATH( + plugins.mockavizta.path, + new Error( + "Cannot find module 'failing-mock' from 'src/lib/initialize.ts'" + ) + ) + ); } }); }); diff --git a/src/__tests__/unit/lib/load.test.ts b/src/__tests__/unit/lib/load.test.ts index 95ae6b26b..ba3bca902 100644 --- a/src/__tests__/unit/lib/load.test.ts +++ b/src/__tests__/unit/lib/load.test.ts @@ -38,14 +38,14 @@ jest.mock('../../../util/yaml', () => ({ }, })); -import {load, loadIfDiffFiles} from '../../../lib/load'; +import {PluginParams} from '@grnsft/if-core/types'; -import {PARAMETERS} from '../../../config'; +import {load, loadIfDiffFiles} from '../../../lib/load'; -import {PluginParams} from '../../../types/interface'; +import {PARAMETERS, STRINGS} from '../../../config'; -import {STRINGS} from '../../../config'; import {parseManifestFromStdin} from '../../../util/helpers'; + import {LoadDiffParams} from '../../../types/util/args'; const {INVALID_SOURCE} = STRINGS; diff --git a/src/__tests__/unit/lib/manifest/basic.ts b/src/__tests__/unit/lib/manifest/basic.ts deleted file mode 100644 index e5da7c642..000000000 --- a/src/__tests__/unit/lib/manifest/basic.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {Manifest} from '../../../../types/manifest'; - -export const manifest: Manifest = { - name: 'gsf-demo', - description: 'Hello', - tags: { - kind: 'web', - complexity: 'moderate', - category: 'cloud', - }, - initialize: { - plugins: { - 'mock-name': { - model: 'MockaviztaModel', - path: 'mock-path', - 'global-config': { - allocation: 'LINEAR', - verbose: true, - }, - }, - }, - }, - tree: { - children: { - 'front-end': { - pipeline: ['mock-name'], - config: { - 'mock-name': { - 'core-units': 24, - processor: 'Intel® Core™ i7-1185G7', - }, - }, - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 3600, - 'cpu/utilization': 18.392, - }, - { - timestamp: '2023-08-06T00:00', - duration: 3600, - 'cpu/utilization': 16, - }, - ], - }, - }, - }, -}; diff --git a/src/__tests__/unit/lib/manifest/nested.ts b/src/__tests__/unit/lib/manifest/nested.ts deleted file mode 100644 index 0c27fad12..000000000 --- a/src/__tests__/unit/lib/manifest/nested.ts +++ /dev/null @@ -1,146 +0,0 @@ -import {Manifest} from '../../../../types/manifest'; - -export const manifestNested: Manifest = { - name: 'nesting-demo', - description: null, - tags: { - kind: 'web', - complexity: 'moderate', - category: 'on-premise', - }, - initialize: { - plugins: { - mockavizta: { - model: 'MockaviztaModel', - path: 'mock-path', - 'global-config': { - allocation: 'LINEAR', - verbose: true, - }, - }, - }, - }, - tree: { - children: { - 'child-0': { - config: { - mockavizta: { - allocation: 'LINEAR', - verbose: true, - }, - }, - pipeline: ['mockavizta'], - children: { - 'child-0-1': { - children: { - 'child-0-1-1': { - pipeline: ['mockavizta'], - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 10, - 'cpu/utilization': 50, - 'energy-network': 0.000811, - carbon: 10, - }, - ], - }, - 'child-0-1-2': { - children: { - 'child-1-2-1': { - pipeline: ['mockavizta'], - config: { - mockavizta: { - allocation: 'mock-allocation', - verbose: false, - }, - }, - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 10, - 'cpu/utilization': 50, - 'energy-network': 0.000811, - carbon: 10, - }, - ], - }, - }, - }, - }, - }, - }, - }, - }, - }, -}; - -export const manifestNestedNoConfig: Manifest = { - name: 'nesting-demo', - description: null, - tags: { - kind: 'web', - complexity: 'moderate', - category: 'on-premise', - }, - initialize: { - plugins: { - mockavizta: { - model: 'MockaviztaModel', - path: 'mock-path', - 'global-config': { - allocation: 'LINEAR', - verbose: true, - }, - }, - }, - }, - tree: { - children: { - 'child-0': { - config: { - mockavizta: { - allocation: 'LINEAR', - verbose: true, - }, - }, - pipeline: ['mockavizta'], - children: { - 'child-0-1': { - children: { - 'child-0-1-1': { - pipeline: ['mockavizta'], - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 10, - 'cpu/utilization': 50, - 'energy-network': 0.000811, - carbon: 10, - }, - ], - }, - 'child-0-1-2': { - children: { - 'child-1-2-1': { - pipeline: ['mockavizta'], - config: {}, - inputs: [ - { - timestamp: '2023-07-06T00:00', - duration: 10, - 'cpu/utilization': 50, - 'energy-network': 0.000811, - carbon: 10, - }, - ], - }, - }, - }, - }, - }, - }, - }, - }, - }, -}; diff --git a/src/__tests__/unit/util/aggregation-helper.test.ts b/src/__tests__/unit/util/aggregation-helper.test.ts index 20690bc2f..1a23bc3c1 100644 --- a/src/__tests__/unit/util/aggregation-helper.test.ts +++ b/src/__tests__/unit/util/aggregation-helper.test.ts @@ -1,11 +1,11 @@ +import {ERRORS} from '@grnsft/if-core/utils'; +import {PluginParams} from '@grnsft/if-core/types'; + import {aggregateInputsIntoOne} from '../../../util/aggregation-helper'; -import {ERRORS} from '../../../util/errors'; import {STRINGS} from '../../../config'; -import {PluginParams} from '../../../types/interface'; - -const {InvalidAggregationParamsError} = ERRORS; +const {InvalidAggregationMethodError, MissingAggregationParamError} = ERRORS; const {INVALID_AGGREGATION_METHOD, METRIC_MISSING} = STRINGS; describe('util/aggregation-helper: ', () => { @@ -20,9 +20,9 @@ describe('util/aggregation-helper: ', () => { try { aggregateInputsIntoOne(inputs, metrics, isTemporal); } catch (error) { - expect(error).toBeInstanceOf(InvalidAggregationParamsError); + expect(error).toBeInstanceOf(InvalidAggregationMethodError); - if (error instanceof InvalidAggregationParamsError) { + if (error instanceof InvalidAggregationMethodError) { expect(error.message).toEqual(INVALID_AGGREGATION_METHOD(metrics[0])); } } @@ -38,9 +38,9 @@ describe('util/aggregation-helper: ', () => { try { aggregateInputsIntoOne(inputs, metrics, isTemporal); } catch (error) { - expect(error).toBeInstanceOf(InvalidAggregationParamsError); + expect(error).toBeInstanceOf(MissingAggregationParamError); - if (error instanceof InvalidAggregationParamsError) { + if (error instanceof MissingAggregationParamError) { expect(error.message).toEqual(METRIC_MISSING(metrics[0], 0)); } } diff --git a/src/__tests__/unit/util/args.test.ts b/src/__tests__/unit/util/args.test.ts index d40d36cae..f4238cbce 100644 --- a/src/__tests__/unit/util/args.test.ts +++ b/src/__tests__/unit/util/args.test.ts @@ -1,5 +1,20 @@ const processRunningPath = process.cwd(); +jest.mock('../../../util/fs', () => ({ + isFileExists: () => { + if (process.env.fileExists === 'true') { + return true; + } + return false; + }, + isDirectoryExists: () => { + if (process.env.directoryExists === 'true') { + return true; + } + return false; + }, +})); + jest.mock('ts-command-line-args', () => ({ __esModule: true, parse: () => { @@ -60,6 +75,27 @@ jest.mock('ts-command-line-args', () => ({ throw new Error('mock-error'); case 'diff-throw': throw 'mock-error'; + /** If-env mocks */ + case 'manifest-install-provided': + return { + install: true, + manifest: 'mock-manifest.yaml', + }; + case 'manifest-is-not-yaml': + return {manifest: 'manifest'}; + case 'manifest-path-invalid': + throw new Error(MANIFEST_NOT_FOUND); + case 'env-throw-error': + throw new Error('mock-error'); + case 'env-throw': + throw 'mock-error'; + /** If-check */ + case 'manifest-is-provided': + return {manifest: 'mock-manifest.yaml'}; + case 'directory-is-provided': + return {directory: '/mock-directory'}; + case 'flags-are-not-provided': + return {manifest: undefined, directory: undefined}; default: return { manifest: 'mock-manifest.yaml', @@ -69,21 +105,33 @@ jest.mock('ts-command-line-args', () => ({ }, })); -import path = require('path'); +import * as path from 'node:path'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {parseIEProcessArgs, parseIfDiffArgs} from '../../../util/args'; -import {ERRORS} from '../../../util/errors'; +import { + parseIEProcessArgs, + parseIfCheckArgs, + parseIfDiffArgs, + parseIfEnvArgs, +} from '../../../util/args'; import {STRINGS} from '../../../config'; -const {CliInputError} = ERRORS; +const { + CliSourceFileError, + ParseCliParamsError, + InvalidDirectoryError, + MissingCliFlagsError, +} = ERRORS; const { MANIFEST_IS_MISSING, - FILE_IS_NOT_YAML, TARGET_IS_NOT_YAML, INVALID_TARGET, SOURCE_IS_NOT_YAML, + MANIFEST_NOT_FOUND, + DIRECTORY_NOT_FOUND, + IF_CHECK_FLAGS_MISSING, } = STRINGS; describe('util/args: ', () => { @@ -106,8 +154,8 @@ describe('util/args: ', () => { try { parseIEProcessArgs(); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error).toEqual(new CliInputError(MANIFEST_IS_MISSING)); + expect(error).toBeInstanceOf(ParseCliParamsError); + expect(error).toEqual(new ParseCliParamsError(MANIFEST_IS_MISSING)); } process.env.result = 'manifest-is-missing'; @@ -115,8 +163,8 @@ describe('util/args: ', () => { try { parseIEProcessArgs(); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error).toEqual(new CliInputError(MANIFEST_IS_MISSING)); + expect(error).toBeInstanceOf(CliSourceFileError); + expect(error).toEqual(new CliSourceFileError(MANIFEST_IS_MISSING)); } }); @@ -195,8 +243,8 @@ describe('util/args: ', () => { try { parseIEProcessArgs(); } catch (error) { - expect(error).toBeInstanceOf(CliInputError); - expect(error).toEqual(new CliInputError(FILE_IS_NOT_YAML)); + expect(error).toBeInstanceOf(CliSourceFileError); + expect(error).toEqual(new CliSourceFileError(SOURCE_IS_NOT_YAML)); } }); @@ -226,7 +274,7 @@ describe('util/args: ', () => { parseIfDiffArgs(); } catch (error) { if (error instanceof Error) { - expect(error).toEqual(new CliInputError(INVALID_TARGET)); + expect(error).toEqual(new ParseCliParamsError(INVALID_TARGET)); } } }); @@ -239,7 +287,7 @@ describe('util/args: ', () => { parseIfDiffArgs(); } catch (error) { if (error instanceof Error) { - expect(error).toEqual(new CliInputError(TARGET_IS_NOT_YAML)); + expect(error).toEqual(new ParseCliParamsError(TARGET_IS_NOT_YAML)); } } }); @@ -260,7 +308,7 @@ describe('util/args: ', () => { parseIfDiffArgs(); } catch (error) { if (error instanceof Error) { - expect(error).toEqual(new CliInputError(SOURCE_IS_NOT_YAML)); + expect(error).toEqual(new ParseCliParamsError(SOURCE_IS_NOT_YAML)); } } }); @@ -282,7 +330,7 @@ describe('util/args: ', () => { parseIfDiffArgs(); } catch (error) { if (error instanceof Error) { - expect(error).toEqual(new CliInputError('mock-error')); + expect(error).toEqual(new ParseCliParamsError('mock-error')); } } }); @@ -299,5 +347,176 @@ describe('util/args: ', () => { }); }); + describe('parseIfEnvArgs(): ', () => { + it('executes if `manifest` is missing.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-is-missing'; + const response = await parseIfEnvArgs(); + + expect.assertions(1); + + expect(response).toEqual({install: undefined}); + }); + + it('executes if `manifest` and `install` are provided.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-install-provided'; + + const response = await parseIfEnvArgs(); + + expect.assertions(2); + expect(response).toHaveProperty('install'); + expect(response).toHaveProperty('manifest'); + }); + + it('throws an error if `manifest` is not a yaml.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-is-not-yaml'; + expect.assertions(1); + + try { + await parseIfEnvArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliSourceFileError(SOURCE_IS_NOT_YAML)); + } + } + }); + + it('throws an error if `manifest` path is invalid.', async () => { + process.env.fileExists = 'false'; + expect.assertions(1); + + try { + await parseIfEnvArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new ParseCliParamsError(MANIFEST_NOT_FOUND)); + } + } + }); + + it('throws an error if parsing failed.', async () => { + process.env.result = 'env-throw-error'; + expect.assertions(1); + + try { + await parseIfEnvArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new ParseCliParamsError('mock-error')); + } + } + }); + + it('throws error if parsing failed (not instance of error).', async () => { + process.env.result = 'env-throw'; + expect.assertions(1); + + try { + await parseIfEnvArgs(); + } catch (error) { + expect(error).toEqual('mock-error'); + } + }); + }); + + describe('parseIfCheckArgs(): ', () => { + it('executes when `manifest` is provided.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-is-provided'; + const response = await parseIfCheckArgs(); + + expect.assertions(1); + + expect(response).toEqual({manifest: 'mock-manifest.yaml'}); + }); + + it('executes when the `directory` is provided.', async () => { + process.env.directoryExists = 'true'; + process.env.result = 'directory-is-provided'; + + const response = await parseIfCheckArgs(); + + expect.assertions(1); + + expect(response).toEqual({directory: '/mock-directory'}); + }); + + it('throws an error when the `directory` does not exist.', async () => { + process.env.directoryExists = 'false'; + process.env.result = 'directory-is-provided'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + expect(error).toEqual(new InvalidDirectoryError(DIRECTORY_NOT_FOUND)); + } + }); + + it('throws an error when both `manifest` and `directory` flags are not provided.', async () => { + process.env.result = 'flags-are-not-provided'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + expect(error).toEqual(new MissingCliFlagsError(IF_CHECK_FLAGS_MISSING)); + } + }); + + it('throws an error if `manifest` is not a yaml.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-is-not-yaml'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliSourceFileError(SOURCE_IS_NOT_YAML)); + } + } + }); + + it('throws an error if `manifest` path is invalid.', async () => { + process.env.fileExists = 'false'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new ParseCliParamsError(MANIFEST_NOT_FOUND)); + } + } + }); + + it('throws an error if parsing failed.', async () => { + process.env.result = 'env-throw-error'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new ParseCliParamsError('mock-error')); + } + } + }); + + it('throws error if parsing failed (not instance of error).', async () => { + process.env.result = 'env-throw'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + expect(error).toEqual('mock-error'); + } + }); + }); + process.env = originalEnv; }); diff --git a/src/__tests__/unit/util/errors.test.ts b/src/__tests__/unit/util/errors.test.ts deleted file mode 100644 index 47e3e9e9a..000000000 --- a/src/__tests__/unit/util/errors.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {ERRORS} from '../../../util/errors'; - -describe('util/errors: ', () => { - describe('ERRORS: ', () => { - it('checks for properties.', () => { - const errors = Object.values(ERRORS); - - errors.forEach(error => { - expect(error).toBeInstanceOf(Function); - }); - }); - }); -}); diff --git a/src/__tests__/unit/util/fs.test.ts b/src/__tests__/unit/util/fs.test.ts new file mode 100644 index 000000000..77b3c0370 --- /dev/null +++ b/src/__tests__/unit/util/fs.test.ts @@ -0,0 +1,144 @@ +import * as fs from 'fs/promises'; + +import { + getFileName, + isDirectoryExists, + isFileExists, + getYamlFiles, + removeFileIfExists, +} from '../../../util/fs'; + +jest.mock('fs/promises', () => require('../../../__mocks__/fs')); + +describe('util/fs: ', () => { + describe('isFileExists(): ', () => { + it('returns true if the file exists.', async () => { + const result = await isFileExists('true'); + + expect.assertions(1); + expect(result).toEqual(true); + }); + + it('returns fale if the file does not exist.', async () => { + const result = await isFileExists('false'); + + expect.assertions(1); + expect(result).toEqual(false); + }); + }); + + describe('isDirectoryExists(): ', () => { + it('returns true if directory exists.', async () => { + const result = await isDirectoryExists('true'); + + expect.assertions(1); + expect(result).toEqual(true); + }); + + it('returns false if directory does not exist.', async () => { + const result = await isDirectoryExists('false'); + + expect.assertions(1); + expect(result).toEqual(false); + }); + }); + + describe('getFileName(): ', () => { + it('returns the file name without extension for a file with an extension.', () => { + const filePath = '/path/to/file/example.yaml'; + const result = getFileName(filePath); + + expect.assertions(1); + expect(result).toBe('example'); + }); + + it('returns the file name without extension for a file with multiple dots.', () => { + const filePath = '/path/to/file/example.test.yaml'; + const result = getFileName(filePath); + expect(result).toBe('example.test'); + }); + + it('returns the file name as is if there is no extension.', () => { + const filePath = '/path/to/file/example'; + const result = getFileName(filePath); + expect(result).toBe('example'); + }); + + it('handles file names with special characters.', () => { + const filePath = + '/path/to/file/complex-file.name.with-multiple.parts.yaml'; + const result = getFileName(filePath); + expect(result).toBe('complex-file.name.with-multiple.parts'); + }); + + it('handles file names with no path.', () => { + const filePath = 'example.yaml'; + const result = getFileName(filePath); + expect(result).toBe('example'); + }); + + it('handles empty string as file path.', () => { + const filePath = ''; + const result = getFileName(filePath); + expect(result).toBe(''); + }); + }); + + describe('getYamlFiles(): ', () => { + it('returns an empty array if the directory is empty.', async () => { + const fsReaddirSpy = jest.spyOn(fs, 'readdir'); + const result = await getYamlFiles('/mock-empty-directory'); + + expect(result).toEqual([]); + expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-empty-directory'); + }); + + it('returns YAML files in the directory', async () => { + const fsReaddirSpy = jest.spyOn(fs, 'readdir'); + jest + .spyOn(fs, 'lstat') + .mockResolvedValue({isDirectory: () => false} as any); + + const result = await getYamlFiles('/mock-directory'); + expect.assertions(2); + expect(result).toEqual([ + '/mock-directory/file1.yaml', + '/mock-directory/file2.yml', + ]); + expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-directory'); + }); + + it('recursively finds YAML files in nested directories.', async () => { + const fsReaddirSpy = jest.spyOn(fs, 'readdir'); + jest + .spyOn(fs, 'lstat') + .mockResolvedValue({isDirectory: () => false} as any); + const result = await getYamlFiles('/mock-sub-directory'); + + expect.assertions(2); + expect(result).toEqual([ + '/mock-sub-directory/subdir/file2.yml', + '/mock-sub-directory/file1.yaml', + ]); + expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-directory'); + }); + }); + + describe('removeFileIfExists(): ', () => { + it('successfully delete file if exists.', async () => { + await isFileExists('true'); + const result = await removeFileIfExists('mock-path'); + + expect.assertions(1); + expect(result).toEqual(undefined); + }); + + it('does not throw an error if the file not exists.', async () => { + await isFileExists('false'); + const result = await removeFileIfExists('mock-path'); + + expect.assertions(1); + expect(result).toEqual(undefined); + }); + }); +}); diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index 58ed0525b..7b6bc2eec 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ const mockWarn = jest.fn(); const mockError = jest.fn(); @@ -10,6 +11,131 @@ jest.mock('../../../util/logger', () => ({ error: mockError, }, })); + +jest.mock('path', () => { + const actualPath = jest.requireActual('path') as Record; + return { + __esModule: true, + ...actualPath, + dirname: jest.fn(() => './mock-path'), + }; +}); + +jest.mock('fs/promises', () => require('../../../__mocks__/fs')); + +jest.mock('../../../lib/load', () => ({ + load: jest.fn(() => { + if (process.env.manifest === 'true') { + return { + rawManifest: { + name: 'divide', + initialize: { + plugins: { + 'cloud-metadata': { + path: '@grnsft/if-plugins', + method: 'CloudMetadata', + }, + divide: { + path: 'builtin', + method: 'Divide', + 'global-config': { + numerator: 'vcpus-allocated', + denominator: 2, + output: 'cpu/number-cores', + }, + }, + }, + }, + execution: { + environment: { + dependencies: [ + '@grnsft/if-core@0.0.7', + '@grnsft/if-plugins@v0.3.2 extraneous -> file:../../../if-models', + '@grnsft/if-unofficial-plugins@v0.3.0 extraneous -> file:../../../if-unofficial-models', + ], + }, + }, + }, + }; + } + return { + rawManifest: { + initialize: { + plugins: {'@grnsft/if-plugins': '1.0.0'}, + }, + execution: { + environment: { + dependencies: [], + }, + }, + }, + }; + }), +})); + +const initPackage = jest.fn(() => Promise.resolve('mock-path')); +const updatePackage = jest.fn(() => Promise.resolve(true)); +const installdeps = jest.fn(); +const updatedeps = jest.fn(); +jest.mock('../../../util/npm', () => { + const actualNPMUtil = jest.requireActual('../../../util/npm'); + + return { + ...actualNPMUtil, + initPackageJsonIfNotExists: (folderPath: string) => { + if (process.env.NPM_MOCK === 'true') { + return initPackage(); + } + + if (process.env.NPM_MOCK === 'error') { + throw new Error('mock-error'); + } + + return actualNPMUtil.initPackageJsonIfNotExists(folderPath); + }, + updatePackageJsonProperties: ( + newPackageJsonPath: string, + appendDependencies: boolean + ) => { + if (process.env.NPM_MOCK === 'true') { + return updatePackage(); + } + + return actualNPMUtil.updatePackageJsonProperties( + newPackageJsonPath, + appendDependencies + ); + }, + installDependencies: ( + folderPath: string, + dependencies: {[path: string]: string} + ) => { + if (process.env.NPM_MOCK === 'true') { + return installdeps(); + } + + return actualNPMUtil.installDependencies(folderPath, dependencies); + }, + updatePackageJsonDependencies: ( + packageJsonPath: string, + dependencies: any, + cwd: boolean + ) => { + if (process.env.NPM_MOCK === 'true') { + return updatedeps(); + } + + return actualNPMUtil.updatePackageJsonDependencies( + packageJsonPath, + dependencies, + cwd + ); + }, + }; +}); + +import {ERRORS} from '@grnsft/if-core/utils'; + import { andHandle, checkIfEqual, @@ -17,10 +143,17 @@ import { mergeObjects, oneIsPrimitive, parseManifestFromStdin, + getOptionsFromArgs, + addTemplateManifest, + initializeAndInstallLibs, + logStdoutFailMessage, } from '../../../util/helpers'; -import {ERRORS} from '../../../util/errors'; +import {CONFIG} from '../../../config'; import {Difference} from '../../../types/lib/compare'; +const {IF_ENV} = CONFIG; +const {FAILURE_MESSAGE_DEPENDENCIES, FAILURE_MESSAGE} = IF_ENV; + const {WriteFileError} = ERRORS; describe('util/helpers: ', () => { @@ -30,15 +163,6 @@ describe('util/helpers: ', () => { mockError.mockReset(); }); - it('logs error and warn in case of error is unknown.', () => { - const message = 'mock-message'; - const MockError = class extends Error {}; - - andHandle(new MockError(message)); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockError).toHaveBeenCalledTimes(1); - }); - it('logs error in case of error is unknown.', () => { const message = 'mock-message'; @@ -47,9 +171,7 @@ describe('util/helpers: ', () => { expect(mockError).toHaveBeenCalledTimes(1); }); }); -}); -describe('util/helpers: ', () => { describe('mergeObjects(): ', () => { it('does not override input.', () => { expect.assertions(1); @@ -408,4 +530,158 @@ description: mock-description expect(response).toBeFalsy(); }); }); + + describe('getOptionsFromArgs(): ', () => { + it('returns the correct options when dependencies are present.', async () => { + const commandArgs = { + manifest: '/path/to/mock-manifest.json', + install: false, + }; + + process.env.manifest = 'true'; + + const result = await getOptionsFromArgs(commandArgs); + expect.assertions(1); + + expect(result).toEqual({ + folderPath: './mock-path', + dependencies: { + '@grnsft/if-plugins': '^v0.3.2', + }, + install: false, + }); + }); + + it('throws an error when there are no dependencies.', async () => { + const commandArgs = { + manifest: '/path/to/mock-manifest.json', + install: false, + }; + + process.env.manifest = 'false'; + + expect.assertions(1); + try { + await getOptionsFromArgs(commandArgs); + } catch (error) { + expect(error).toEqual(new Error(FAILURE_MESSAGE_DEPENDENCIES)); + } + }); + }); + + describe('addTemplateManifest(): ', () => { + it('successfully adds the template manifest to the directory.', async () => { + await addTemplateManifest('./'); + + expect.assertions(1); + }); + + it('throws an error when the manifest is not added into the directory.', async () => { + expect.assertions(1); + + try { + await addTemplateManifest(''); + } catch (error) { + const logSpy = jest.spyOn(global.console, 'log'); + expect(logSpy).toEqual(FAILURE_MESSAGE); + } + }); + }); + + describe('initializeAndInstallLibs(): ', () => { + beforeEach(() => { + initPackage.mockReset(); + updatePackage.mockReset(); + installdeps.mockReset(); + updatedeps.mockReset(); + }); + + it('installs dependencies if install flag is truthy.', async () => { + process.env.NPM_MOCK = 'true'; + // @ts-ignore + process.exit = (code: any) => code; + const options = { + folderPath: 'mock-folderPath', + install: true, + cwd: true, + dependencies: { + mock: 'mock-dependencies', + }, + }; + + expect.assertions(4); + await initializeAndInstallLibs(options); + + expect(initPackage).toHaveBeenCalledTimes(1); + expect(updatePackage).toHaveBeenCalledTimes(1); + expect(installdeps).toHaveBeenCalledTimes(1); + expect(updatedeps).toHaveBeenCalledTimes(0); + }); + + it('updates dependencies if install flag is falsy.', async () => { + process.env.NPM_MOCK = 'true'; + // @ts-ignore + process.exit = (code: any) => code; + const options = { + folderPath: 'mock-folderPath', + install: false, + cwd: true, + dependencies: { + mock: 'mock-dependencies', + }, + }; + + expect.assertions(4); + await initializeAndInstallLibs(options); + + expect(initPackage).toHaveBeenCalledTimes(1); + expect(updatePackage).toHaveBeenCalledTimes(1); + expect(installdeps).toHaveBeenCalledTimes(0); + expect(updatedeps).toHaveBeenCalledTimes(1); + }); + + it('exits process if error is thrown.', async () => { + process.env.NPM_MOCK = 'error'; + const originalProcessExit = process.exit; + const mockExit = jest.fn(); + // @ts-ignore + process.exit = mockExit; + const options = { + folderPath: 'mock-folderPath', + install: false, + cwd: true, + dependencies: { + mock: 'mock-dependencies', + }, + }; + + expect.assertions(5); + await initializeAndInstallLibs(options); + + expect(initPackage).toHaveBeenCalledTimes(0); + expect(updatePackage).toHaveBeenCalledTimes(0); + expect(installdeps).toHaveBeenCalledTimes(0); + expect(updatedeps).toHaveBeenCalledTimes(0); + expect(mockExit).toHaveBeenCalledTimes(1); + + process.exit = originalProcessExit; + }); + }); + + describe('logStdoutFailMessage(): ', () => { + it('successfully logs the failed message.', () => { + const errorMessage = {stdout: '\n\nmock error message'}; + const mockFilename = 'mock-filename.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + logStdoutFailMessage(errorMessage, mockFilename); + + expect.assertions(2); + + expect(logSpy).toHaveBeenCalledWith( + `if-check could not verify ${mockFilename}. The re-executed file does not match the original.\n` + ); + + expect(logSpy).toHaveBeenCalledWith('mock error message'); + }); + }); }); diff --git a/src/__tests__/unit/util/npm.test.ts b/src/__tests__/unit/util/npm.test.ts new file mode 100644 index 000000000..b7f028fa4 --- /dev/null +++ b/src/__tests__/unit/util/npm.test.ts @@ -0,0 +1,271 @@ +import * as fs from 'fs/promises'; +import * as fsSync from 'fs'; +import * as path from 'path'; + +jest.mock('fs/promises', () => require('../../../__mocks__/fs')); + +const mockInfo = jest.fn(); + +jest.mock('node:readline/promises', () => + require('../../../__mocks__/readline') +); +jest.mock('../../../util/logger', () => ({ + logger: { + info: mockInfo, + }, +})); + +import { + installDependencies, + initPackageJsonIfNotExists, + updatePackageJsonDependencies, + extractPathsWithVersion, + updatePackageJsonProperties, + executeCommands, +} from '../../../util/npm'; +import {isFileExists} from '../../../util/fs'; + +import {STRINGS} from '../../../config/strings'; +import {ManifestPlugin} from '../../../types/npm'; + +const {INITIALIZING_PACKAGE_JSON, INSTALLING_NPM_PACKAGES} = STRINGS; + +describe('util/npm: ', () => { + const helpers = require('../../../util/helpers'); + const folderPath = path.resolve(__dirname, 'npm-test'); + + beforeAll(() => { + if (!fsSync.existsSync(folderPath)) { + fsSync.mkdirSync(folderPath, {recursive: true}); + } + }); + + afterAll(() => { + if (fsSync.existsSync(folderPath)) { + fsSync.rmSync(folderPath, {recursive: true, force: true}); + } + }); + + describe('initPackageJsonIfNotExists(): ', () => { + it('initializes package.json if it does not exist.', async () => { + const spyExecPromise = jest.spyOn(helpers, 'execPromise'); + isFileExists('true'); + + await initPackageJsonIfNotExists(folderPath); + + expect.assertions(2); + expect(mockInfo).toHaveBeenCalledWith(INITIALIZING_PACKAGE_JSON); + expect(spyExecPromise).toHaveBeenCalledWith('npm init -y', { + cwd: folderPath, + }); + }); + + it('returns the package.json path if it exists.', async () => { + const packageJsonPath = path.resolve(folderPath, 'package.json'); + isFileExists('false'); + + const result = await initPackageJsonIfNotExists(folderPath); + + expect.assertions(1); + expect(result).toBe(packageJsonPath); + }); + }); + + describe('installDependencies(): ', () => { + const dependencies = { + '@grnsft/if': '^0.3.3-beta.0', + }; + + it('calls execPromise with the correct arguments.', async () => { + const spyExecPromise = jest.spyOn(helpers, 'execPromise'); + const formattedDependencies = ['@grnsft/if@0.3.3-beta.0']; + expect.assertions(1); + + await installDependencies(folderPath, dependencies); + + expect(spyExecPromise).toHaveBeenCalledWith( + `npm install ${formattedDependencies.join(' ')}`, + {cwd: folderPath} + ); + }, 30000); + + it('logs the installation message.', async () => { + const dependencies = { + '@grnsft/if': '^0.3.3-beta.0', + }; + + await installDependencies(folderPath, dependencies); + + expect.assertions(1); + expect(mockInfo).toHaveBeenCalledWith(INSTALLING_NPM_PACKAGES); + }); + }); + + describe('updatePackageJsonDependencies(): ', () => { + it('successfully updates the package.json dependencies when cwd is false.', async () => { + const dependencies = { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }; + const packageJsonPath = path.join(folderPath, 'package.json-npm'); + + const expectedPackageJsonContent = JSON.stringify( + { + dependencies: { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }, + }, + null, + 2 + ); + + const fsReadSpy = jest + .spyOn(fs, 'readFile') + .mockResolvedValue(expectedPackageJsonContent); + await updatePackageJsonDependencies(packageJsonPath, dependencies, false); + + expect.assertions(2); + + expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); + }); + + it('successfully updates the package.json dependencies when cwd is true.', async () => { + const dependencies = { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }; + const packageJsonPath = path.join(folderPath, 'package.json-npm'); + + const expectedPackageJsonContent = JSON.stringify( + { + dependencies: { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }, + }, + null, + 2 + ); + + const fsReadSpy = jest + .spyOn(fs, 'readFile') + .mockResolvedValue(expectedPackageJsonContent); + await updatePackageJsonDependencies(packageJsonPath, dependencies, true); + + expect.assertions(2); + + expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); + }); + }); + + describe('extractPathsWithVersion(): ', () => { + it('extracts paths with correct versions.', () => { + const plugins: ManifestPlugin = { + 'cloud-metadata': { + path: '@grnsft/if-plugins', + method: 'CloudMetadata', + }, + divide: { + path: 'builtin', + method: 'Divide', + }, + 'boavizta-cpu': { + path: '@grnsft/if-unofficial-plugins', + method: 'BoaviztaCpuOutput', + }, + }; + const dependencies = [ + '@babel/core@7.22.10', + '@babel/preset-typescript@7.23.3', + '@commitlint/cli@18.6.0', + '@commitlint/config-conventional@18.6.0', + '@grnsft/if-core@0.0.7', + '@grnsft/if-plugins@v0.3.2', + '@grnsft/if-unofficial-plugins@v0.3.0 extraneous -> file:../../../if-unofficial-models', + '@jest/globals@29.7.0', + ]; + + const result = extractPathsWithVersion(plugins, dependencies); + + expect.assertions(1); + expect(result).toEqual({ + '@grnsft/if-plugins': '^v0.3.2', + '@grnsft/if-unofficial-plugins': '^v0.3.0', + }); + }); + + it('returns an empty object if no matches found', () => { + const plugins: ManifestPlugin = { + 'cloud-metadata': { + path: '@grnsft/if-plugins', + method: 'CloudMetadata', + }, + divide: { + path: 'builtin', + method: 'Divide', + }, + 'boavizta-cpu': { + path: '@grnsft/if-unofficial-plugins', + method: 'BoaviztaCpuOutput', + }, + }; + const dependencies = [ + '@babel/core@7.22.10', + '@babel/preset-typescript@7.23.3', + ]; + + expect.assertions(1); + const result = extractPathsWithVersion(plugins, dependencies); + expect(result).toEqual({}); + }); + }); + + describe('updatePackageJsonProperties(): ', () => { + it('updates the package.json properties correctly.', async () => { + const packageJsonPath = path.join(folderPath, 'package.json-npm1'); + + const expectedPackageJsonContent = JSON.stringify( + { + name: 'if-environment', + description: 'mock-description', + author: {}, + bugs: {}, + engines: {}, + homepage: 'mock-homepage', + dependencies: { + '@grnsft/if-plugins': '^0.3.3-beta.0', + }, + }, + null, + 2 + ); + + const fsReadSpy = jest + .spyOn(fs, 'readFile') + .mockResolvedValue(expectedPackageJsonContent); + await updatePackageJsonProperties(packageJsonPath, true); + + expect.assertions(8); + + expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); + }); + }); + + describe('executeCommands(): ', () => { + it('successfully executes with correct commands.', async () => { + const manifest = './src/__mocks__/mock-manifest.yaml'; + const reManifest = 'src/__mocks__/re-mock-manifest.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + + jest.spyOn(fs, 'unlink').mockResolvedValue(); + + await executeCommands(manifest, false); + + expect.assertions(1); + expect(logSpy).toHaveBeenCalledWith( + 'if-check successfully verified mock-manifest.yaml\n' + ); + + const packageJsonPath = 'src/__mocks__/package.json'; + fsSync.unlink(path.resolve(process.cwd(), reManifest), () => {}); + fsSync.unlink(path.resolve(process.cwd(), packageJsonPath), () => {}); + }, 70000); + }); +}); diff --git a/src/__tests__/unit/util/plugin-storage.test.ts b/src/__tests__/unit/util/plugin-storage.test.ts index b2bd25011..8b76f36cd 100644 --- a/src/__tests__/unit/util/plugin-storage.test.ts +++ b/src/__tests__/unit/util/plugin-storage.test.ts @@ -1,7 +1,8 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {pluginStorage} from '../../../util/plugin-storage'; -import {ERRORS} from '../../../util/errors'; -const {PluginInitalizationError} = ERRORS; +const {PluginInitializationError} = ERRORS; describe('util/pluginStorage: ', () => { describe('pluginStorage(): ', () => { @@ -30,9 +31,9 @@ describe('util/pluginStorage: ', () => { try { storage.get(pluginName); } catch (error) { - expect(error).toBeInstanceOf(PluginInitalizationError); + expect(error).toBeInstanceOf(PluginInitializationError); - if (error instanceof PluginInitalizationError) { + if (error instanceof PluginInitializationError) { expect(error.message).toEqual; } } diff --git a/src/builtins/README.md b/src/builtins/README.md index b9906921d..c37e143a1 100644 --- a/src/builtins/README.md +++ b/src/builtins/README.md @@ -305,7 +305,7 @@ Then, you must select the metric you want to export to CSV. The name of that met For example, to export the `carbon` data from your tree to a CSV file: ```sh -ie --manifest example.yml --output example#carbon +if-run --manifest example.yml --output example#carbon ``` This will save a CSV file called `example.csv`. The contents will look similar to the following: diff --git a/src/builtins/coefficient/README.md b/src/builtins/coefficient/README.md index 096d20ab5..4579cd5b8 100644 --- a/src/builtins/coefficient/README.md +++ b/src/builtins/coefficient/README.md @@ -86,7 +86,25 @@ tree: You can run this example by saving it as `./examples/manifests/coefficient.yml` and executing the following command from the project root: ```sh -ie --manifest ./examples/manifests/coefficient.yml --output ./examples/outputs/coefficient.yml +if-run --manifest ./examples/manifests/coefficient.yml --output ./examples/outputs/coefficient.yml ``` The results will be saved to a new `yaml` file in `./examples/outputs` + + +## Errors + +`Coefficient` exposes one of the IF error classes. + +### GlobalConfigError + +You will receive an error starting `GlobalConfigError: ` if you have not provided the expected configuration data in the plugin's `initialize` block. + +The required parameters are: +- `input-parameter`: this must be a string +- `coefficient`: this must be a number +- `output-parameter`: this must be a string + +You can fix this error by checking you are providing valid values for each parameter in the config. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/coefficient/index.ts b/src/builtins/coefficient/index.ts index 627e5f64e..2d4f6a1c9 100644 --- a/src/builtins/coefficient/index.ts +++ b/src/builtins/coefficient/index.ts @@ -1,13 +1,17 @@ import {z} from 'zod'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; +import {ERRORS} from '@grnsft/if-core/utils'; +import { + CoefficientConfig, + ExecutePlugin, + PluginParams, +} from '@grnsft/if-core/types'; import {validate} from '../../util/validations'; -import {ERRORS} from '../../util/errors'; -import {CoefficientConfig} from './types'; +import {STRINGS} from '../../config'; -const {ConfigNotFoundError} = ERRORS; +const {GlobalConfigError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; export const Coefficient = (globalConfig: CoefficientConfig): ExecutePlugin => { const metadata = { @@ -24,6 +28,8 @@ export const Coefficient = (globalConfig: CoefficientConfig): ExecutePlugin => { const coefficient = safeGlobalConfig['coefficient']; return inputs.map(input => { + validateSingleInput(input, inputParameter); + return { ...input, [outputParameter]: calculateProduct(input, inputParameter, coefficient), @@ -31,6 +37,19 @@ export const Coefficient = (globalConfig: CoefficientConfig): ExecutePlugin => { }); }; + /** + * Checks for required fields in input. + */ + const validateSingleInput = (input: PluginParams, inputParameter: string) => { + const inputData = { + 'input-parameter': input[inputParameter], + }; + const validationSchema = z.record(z.string(), z.number()); + validate(validationSchema, inputData); + + return input; + }; + /** * Calculates the product of the energy components. */ @@ -45,7 +64,7 @@ export const Coefficient = (globalConfig: CoefficientConfig): ExecutePlugin => { */ const validateGlobalConfig = () => { if (!globalConfig) { - throw new ConfigNotFoundError('Global config is not provided.'); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } const globalConfigSchema = z.object({ diff --git a/src/builtins/coefficient/types.ts b/src/builtins/coefficient/types.ts deleted file mode 100644 index 9cbb78b7d..000000000 --- a/src/builtins/coefficient/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type CoefficientConfig = { - 'input-parameter': string; - coefficient: number; - 'output-parameter': string; -}; diff --git a/src/builtins/copy-param/README.md b/src/builtins/copy-param/README.md new file mode 100644 index 000000000..78ab5f656 --- /dev/null +++ b/src/builtins/copy-param/README.md @@ -0,0 +1,104 @@ +# Copy-param + +`copy-param` is a generic plugin that duplicates an existing parameter in the `input` data and assigns it to a new key. You can either keep or delete the original copied parameter. A common use case for this is to rename parameters in the `inputs` array. + +You provide the name of the value you want to copy, and a name to assign the copy to. You also toggle a `keep-existing` parameter to either persist or delete the original copied value. + +For example, you could copy `energy` into `energy-copy`, with `keep-existing=true`. In this case your inputs: + +```yaml +- timestamp: "2023-12-12T00:00:13.000Z", + duration: 30, + energy: 30 +``` + +would become + +```yaml +- timestamp: "2023-12-12T00:00:13.000Z", + duration: 30, + energy: 30 + energy-copy: 30 +``` + +but with `keep-existing=false`, the same inputs would yield: + +```yaml +- timestamp: "2023-12-12T00:00:13.000Z", + duration: 30, + energy-copy: 30 +``` + +## Parameters + +### Config + +Three parameters are required in config: `from` and `to` and `keep-existing`. + +`from`: an array of strings. Each string should match an existing key in the `inputs` array +`to`: a string defining the name to use to add the result of summing the input parameters to the output array. +`keep-existing`: toggles whether to keep or delete the copied parameter (defined in `to`) + +### Inputs + +As with all plugins, `timestamp` and `duration` are required. The key passed to `from` must exist in the `input` data. + +## Returns + +The plugin adds a new parameter with the name defined in `to` to the `input` data. + + +## Implementation + +To run the plugin, you must first create an instance of `Copy`. Then, you can call `execute()`. + +```typescript +import { Copy } from "."; + +const plugin = Copy({ 'keep-existing': true, from: 'from-param', to: 'to-param' }); + +const result = plugin.execute([{ + timestamp: "2023-12-12T00:00:13.000Z", + duration: 30, + 'from-param': 'hello', +}]) + +console.log(result) + +``` + +## Example manifest + +IF users will typically call the plugin as part of a pipeline defined in a manifest file. In this case, instantiating the plugin is handled by and does not have to be done explicitly by the user. The following is an example manifest that calls `copy-param`: + +```yaml +name: copy-param +description: +tags: +initialize: + plugins: + copy-param: + path: builtin + method: Copy + global-config: + keep-existing: true + from: original + to: copy +tree: + children: + child-1: + pipeline: + - copy-param + inputs: + - timestamp: "2023-12-12T00:00:00.000Z" + original: 'hello' + +``` + +You can run this example by saving it as `./manifests/examples/copy.yml` and executing the following command from the project root: + +```sh +if-run --manifest ./manifests/examples/copy.yml -s +``` + +The results will be displayed in the console. diff --git a/src/builtins/copy-param/index.ts b/src/builtins/copy-param/index.ts new file mode 100644 index 000000000..6301bbf25 --- /dev/null +++ b/src/builtins/copy-param/index.ts @@ -0,0 +1,89 @@ +import {z} from 'zod'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams} from '@grnsft/if-core/types'; + +import {validate} from '../../util/validations'; + +import {STRINGS} from '../../config'; + +const {MISSING_GLOBAL_CONFIG} = STRINGS; +const {GlobalConfigError} = ERRORS; +// keep-existing: true/false (whether to remove the parameter you are copying from) +// from-param: the parameter you are copying from (e.g. cpu/name) +// to-field: the parameter you are copying to (e.g. cpu/processor-name) + +export const Copy = (globalConfig: Record): ExecutePlugin => { + const metadata = { + kind: 'execute', + }; + + /** + * Checks global config value are valid. + */ + const validateGlobalConfig = () => { + if (!globalConfig) { + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); + } + + const globalConfigSchema = z.object({ + 'keep-existing': z.boolean(), + from: z.string().min(1), + to: z.string().min(1), + }); + + return validate>( + globalConfigSchema, + globalConfig + ); + }; + + /** + * Checks for required fields in input. + */ + const validateSingleInput = ( + input: PluginParams, + inputParameters: string[] + ) => { + const inputData = inputParameters.reduce( + (acc, param) => { + acc[param] = input[param]; + + return acc; + }, + {} as Record + ); + + const validationSchema = z.record(z.string(), z.string()); + + validate(validationSchema, inputData); + + return input; + }; + + const execute = (inputs: PluginParams[]) => { + const safeGlobalConfig = validateGlobalConfig(); + const keepExisting = safeGlobalConfig['keep-existing'] === true; + const from = safeGlobalConfig['from']; + const to = safeGlobalConfig['to']; + + return inputs.map(input => { + const safeInput = validateSingleInput(input, [from]); + + const outputValue = safeInput[from]; + if (safeInput[from]) { + if (!keepExisting) { + delete safeInput[from]; + } + } + return { + ...safeInput, // need to return or what you provide won't be outputted, don't be evil! + [to]: outputValue, + }; + }); + }; + + return { + metadata, + execute, + }; +}; diff --git a/src/builtins/csv-lookup/README.md b/src/builtins/csv-lookup/README.md index 2c52969ae..3663f4123 100644 --- a/src/builtins/csv-lookup/README.md +++ b/src/builtins/csv-lookup/README.md @@ -136,7 +136,45 @@ You can run this example by saving it as `./examples/manifests/csv-lookup.yml` a ```sh npm i -g @grnsft/if -ie --manifest manifests/plugins/csv-lookup.yml --output manifests/outputs/csv-lookup +if-run --manifest manifests/plugins/csv-lookup.yml --output manifests/outputs/csv-lookup ``` The results will be saved to a new `yaml` file in `manifests/outputs`. + + +## Errors + +Coefficient exposes six of the IF error classes. + +### FetchingFileError + +This error is caused by problems finding the file at the path provided in the `filepath`. If the file is on your local filesystem, you can check that the file is definitely there. For a remote file, check your internet connection. You can check your connection to the server using a tool such as `ping` or `curl`. if you still experience problems, you could retrieve the remote file onto your local filesystem using a tool such as `wget`. + +### ReadFileError, + +This error is caused by problems reading the CSV file provided in the `filepath`. To fix it, check that the file contains valid CSV data. The file should have a `.csv` file extension and the data inside should be formatted correctly. + +### MissingCSVColumnError, + +This error is caused by `CsvLookup` failing to find a column in the CSV file whose name matches what was provided in `query`. To debug, check that you do not have any typos in your `query` and confirm that the requested column name definitely exists in the target file. + +### QueryDataNotFoundError, + +This error is caused by the `CsvLookup` plugin failing to find data that matches your query. Try revising your query parameters. + +### CSVParseError, + +This error arises due to problems parsing CSV data into IF. This can occur when the CSV data is incorrectly formatted or contains unexpected characters that IF does not recognize. These errors are expected to be unusual edge cases as incorrectly formatted data will usually be identified during file loading and cause a `ReadFileError`. To debug, check your CSV file for any unexpected formatting or unusual characters. + +### GlobalConfigError + +You will receive an error starting `GlobalConfigError: ` if you have not provided the expected configuration data in the plugin's `initialize` block. + +The required parameters are: +- `filepath`: This must be a path to a csv file +- `query`: this must be an array of key-value pairs where the key is a string containing a column name an the value is a string containing the name of a value in `inputs` +- `output`: this must be a string containing a name or a wildcard character (`"*"`) + +You can fix this error by checking you are providing valid values for each parameter in the config. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/csv-lookup/index.ts b/src/builtins/csv-lookup/index.ts index 2539a14cf..8243a6391 100644 --- a/src/builtins/csv-lookup/index.ts +++ b/src/builtins/csv-lookup/index.ts @@ -4,13 +4,29 @@ import {readFile} from 'fs/promises'; import axios from 'axios'; import {z} from 'zod'; import {parse} from 'csv-parse/sync'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams} from '@grnsft/if-core/types'; import {validate} from '../../util/validations'; -import {ERRORS} from '../../util/errors'; -const {ConfigNotFoundError, FileNotFoundError, InputValidationError} = ERRORS; +import {STRINGS} from '../../config'; + +const { + FILE_FETCH_FAILED, + FILE_READ_FAILED, + MISSING_CSV_COLUMN, + NO_QUERY_DATA, + MISSING_GLOBAL_CONFIG, +} = STRINGS; + +const { + FetchingFileError, + ReadFileError, + MissingCSVColumnError, + QueryDataNotFoundError, + GlobalConfigError, + CSVParseError, +} = ERRORS; export const CSVLookup = (globalConfig: any): ExecutePlugin => { const metadata = { @@ -36,18 +52,16 @@ export const CSVLookup = (globalConfig: any): ExecutePlugin => { const retrieveFile = async (filepath: string) => { if (isURL(filepath)) { const {data} = await axios.get(filepath).catch(error => { - throw new FileNotFoundError(`Something went wrong while reading the file: ${filepath}. - ${error.response.message}`); + throw new FetchingFileError( + FILE_FETCH_FAILED(filepath, error.response.message) + ); }); return data; } return readFile(filepath).catch(error => { - throw new FileNotFoundError( - `Something went wrong while reading the file: ${filepath}. -${error}` - ); + throw new ReadFileError(FILE_READ_FAILED(filepath, error)); }); }; @@ -81,7 +95,7 @@ ${error}` */ const fieldAccessor = (field: string, object: any) => { if (!(`${field}` in object)) { - throw new InputValidationError(`There is no column with name: ${field}.`); + throw new MissingCSVColumnError(MISSING_CSV_COLUMN(field)); } return nanifyEmptyValues(object[field]); @@ -149,6 +163,24 @@ ${error}` return ifMatchesCriteria.every(value => value === true); }; + /** + * Parses CSV file. + */ + const parseCSVFile = (file: string | Buffer) => { + try { + const parsedCSV: any[] = parse(file, { + columns: true, + skip_empty_lines: true, + cast: true, + }); + + return parsedCSV; + } catch (error: any) { + console.error(error); + throw new CSVParseError(error); + } + }; + /** * 1. Validates global config. * 2. Tries to retrieve given file (with url or local path). @@ -160,41 +192,29 @@ ${error}` const {filepath, query, output} = safeGlobalConfig; const file = await retrieveFile(filepath); - - try { - const parsedCSV: any[] = parse(file, { - columns: true, - skip_empty_lines: true, - cast: true, + const parsedCSV = parseCSVFile(file); + + return inputs.map(input => { + /** Collects query values from input. */ + const queryData: any = {}; + const queryKeys = Object.keys(query); + queryKeys.forEach(queryKey => { + const queryValue = query[queryKey]; + queryData[queryKey] = input[queryValue]; }); - return inputs.map(input => { - /** Collects query values from input. */ - const queryData: any = {}; - const queryKeys = Object.keys(query); - queryKeys.forEach(queryKey => { - const queryValue = query[queryKey]; - queryData[queryKey] = input[queryValue]; - }); - - /** Gets related data from CSV. */ - const relatedData = parsedCSV.find(withCriteria(queryData)); + /** Gets related data from CSV. */ + const relatedData = parsedCSV.find(withCriteria(queryData)); - if (!relatedData) { - throw new InputValidationError( - 'One or more of the given query parameters are not found in the target CSV file column headers.' - ); - } + if (!relatedData) { + throw new QueryDataNotFoundError(NO_QUERY_DATA); + } - return { - ...input, - ...filterOutput(relatedData, {output, query}), - }; - }); - } catch (error) { - throw new InputValidationError(`Error happened while parsing given CSV file: ${filepath} -${error}`); - } + return { + ...input, + ...filterOutput(relatedData, {output, query}), + }; + }); }; /** @@ -202,7 +222,7 @@ ${error}`); */ const validateGlobalConfig = () => { if (!globalConfig) { - throw new ConfigNotFoundError('Global config is not provided.'); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } const globalConfigSchema = z.object({ diff --git a/src/builtins/divide/README.md b/src/builtins/divide/README.md index d403d0361..9a4d04f36 100644 --- a/src/builtins/divide/README.md +++ b/src/builtins/divide/README.md @@ -4,8 +4,6 @@ You provide the names of the values you want to divide, and a name to use to add the divide to the output array. -For example, `boavizta-cpu` need `cpu/number-cores` to work, however `cloud-metadata` returns `vcpus-allocated`, to get number of cores you divide `vcpus-allocated` by 2. - ## Parameters ### Plugin config @@ -26,6 +24,8 @@ For example, `boavizta-cpu` need `cpu/number-cores` to work, however `cloud-meta The plugin throws an exception if the division result is not a number. +>Note: Plugin will warn and return `numerator` value in case if `denominator` is zero. + ## Calculation ```pseudocode @@ -89,7 +89,33 @@ You can run this example by saving it as `./examples/manifests/divide.yml` and e ```sh npm i -g @grnsft/if -ie --manifest ./examples/manifests/divide.yml --output ./examples/outputs/divide.yml +if-run --manifest ./examples/manifests/divide.yml --output ./examples/outputs/divide.yml ``` The results will be saved to a new `yaml` file in `./examples/outputs`. + +## Errors + +`Divide` exposes two of IF's error classes. + +### GlobalConfigError + +You will receive an error starting `GlobalConfigError: ` if you have not provided the expected configuration data in the plugin's `initialize` block. + +The required parameters are: +- `numerator`: a string containing the name of the input parameter whose value should be divided by `denominator` +- `denominator`: a number to use as the denominator +- ``output`: a string containing the name to assign the result of the division + +You can fix this error by checking you are providing valid values for each parameter in the config. + +### `MissingInputDataError` + +This error arises when a necessary piece of input data is missing from the `inputs` array. +Every element in the ``inputs` array must contain: +- `timestamp` +- `duration` +- whatever value you passed to `numerator` + + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/divide/index.ts b/src/builtins/divide/index.ts index b91d4c9fa..453a50921 100644 --- a/src/builtins/divide/index.ts +++ b/src/builtins/divide/index.ts @@ -1,11 +1,13 @@ import {z} from 'zod'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams, ConfigParams} from '@grnsft/if-core/types'; -import {ERRORS} from '../../util/errors'; import {validate} from '../../util/validations'; -import {ExecutePlugin, PluginParams, ConfigParams} from '../../types/interface'; +import {STRINGS} from '../../config'; -const {InputValidationError, ConfigNotFoundError} = ERRORS; +const {GlobalConfigError, MissingInputDataError} = ERRORS; +const {MISSING_GLOBAL_CONFIG, MISSING_INPUT_DATA, ZERO_DIVISION} = STRINGS; export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { const metadata = { @@ -19,16 +21,16 @@ export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { const safeGlobalConfig = validateGlobalConfig(); const {numerator, denominator, output} = safeGlobalConfig; - return inputs.map(input => { + return inputs.map((input, index) => { const safeInput = Object.assign( {}, input, - validateSingleInput(input, numerator, denominator) + validateSingleInput(input, {numerator, denominator}) ); return { ...input, - [output]: calculateDivide(safeInput, numerator, denominator), + [output]: calculateDivide(safeInput, index, {numerator, denominator}), }; }); }; @@ -38,12 +40,12 @@ export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { */ const validateGlobalConfig = () => { if (!globalConfig) { - throw new ConfigNotFoundError('Global config is not provided.'); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } const schema = z.object({ numerator: z.string().min(1), - denominator: z.string().or(z.number().gt(0)), + denominator: z.string().or(z.number()), output: z.string(), }); @@ -55,9 +57,13 @@ export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { */ const validateSingleInput = ( input: PluginParams, - numerator: string, - denominator: number | string + params: { + numerator: string; + denominator: number | string; + } ) => { + const {numerator, denominator} = params; + const schema = z .object({ [numerator]: z.number(), @@ -65,10 +71,9 @@ export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { }) .refine(() => { if (typeof denominator === 'string' && !input[denominator]) { - throw new InputValidationError( - `\`${denominator}\` is missing from the input.` - ); + throw new MissingInputDataError(MISSING_INPUT_DATA(denominator)); } + return true; }); @@ -80,9 +85,22 @@ export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { */ const calculateDivide = ( input: PluginParams, - numerator: string, - denominator: number | string - ) => input[numerator] / (input[denominator] || denominator); + index: number, + params: { + numerator: string; + denominator: number | string; + } + ) => { + const {denominator, numerator} = params; + const finalDenominator = input[denominator] || denominator; + + if (finalDenominator === 0) { + console.warn(ZERO_DIVISION(Divide.name, index)); + return input[numerator]; + } + + return input[numerator] / finalDenominator; + }; return { metadata, diff --git a/src/builtins/exponent/README.md b/src/builtins/exponent/README.md index 48a67965a..91d938cef 100644 --- a/src/builtins/exponent/README.md +++ b/src/builtins/exponent/README.md @@ -91,7 +91,27 @@ You can run this example by saving it as `manifests/examples/test/exponent.yml` ```sh npm i -g @grnsft/if -ie --manifest manifests/examples/test/exponent.yml --output manifests/outputs/exponent.yml +if-run --manifest manifests/examples/test/exponent.yml --output manifests/outputs/exponent.yml ``` The results will be saved to a new `yaml` file in `manifests/outputs`. + +## Errors + +`Exponent` exposes two of IF's error classes. + +### `MissingInputDataError` + +This error arises when a necessary piece of input data is missing from the `inputs` array. +Every element in the `inputs` array must contain: + +- `timestamp` +- `duration` +- whatever value you passed to `input-parameter` + + +### `InputValidationError` + +This error arises when an invalid value is passed to `Exponent`. Typically, this can occur when a non-numeric value (such as a string made of alphabetic characters) is passed where a number or numeric string is expected. Please check that the types are correct for all the relevant fields in your `inputs` array. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/exponent/index.ts b/src/builtins/exponent/index.ts index 404161e63..ce4e959ef 100644 --- a/src/builtins/exponent/index.ts +++ b/src/builtins/exponent/index.ts @@ -1,16 +1,13 @@ import {z} from 'zod'; +import { + ExecutePlugin, + PluginParams, + ExponentConfig, +} from '@grnsft/if-core/types'; -import {ERRORS} from '../../util/errors'; -import {buildErrorMessage} from '../../util/helpers'; import {validate} from '../../util/validations'; -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {ExponentConfig} from './types'; - -const {InputValidationError} = ERRORS; - export const Exponent = (globalConfig: ExponentConfig): ExecutePlugin => { - const errorBuilder = buildErrorMessage(Exponent.name); const metadata = { kind: 'execute', }; @@ -21,7 +18,7 @@ export const Exponent = (globalConfig: ExponentConfig): ExecutePlugin => { const validateGlobalConfig = () => { const globalConfigSchema = z.object({ 'input-parameter': z.string().min(1), - exponent: z.number().min(1), + exponent: z.number(), 'output-parameter': z.string().min(1), }); @@ -35,28 +32,13 @@ export const Exponent = (globalConfig: ExponentConfig): ExecutePlugin => { * Checks for required fields in input. */ const validateSingleInput = (input: PluginParams, inputParameter: string) => { - validateParamExists(input, inputParameter); - validateNumericString(input[inputParameter]); - }; + const inputData = { + 'input-parameter': input[inputParameter], + }; + const validationSchema = z.record(z.string(), z.number()); + validate(validationSchema, inputData); - const validateParamExists = (input: PluginParams, param: string) => { - if (input[param] === undefined) { - throw new InputValidationError( - errorBuilder({ - message: `${param} is missing from the input array`, - }) - ); - } - }; - - const validateNumericString = (str: string) => { - if (isNaN(+Number(str))) { - throw new InputValidationError( - errorBuilder({ - message: `${str} is not numeric`, - }) - ); - } + return input; }; /** @@ -65,9 +47,10 @@ export const Exponent = (globalConfig: ExponentConfig): ExecutePlugin => { const execute = (inputs: PluginParams[]): PluginParams[] => { const { 'input-parameter': inputParameter, - exponent: exponent, + exponent, 'output-parameter': outputParameter, } = validateGlobalConfig(); + return inputs.map(input => { validateSingleInput(input, inputParameter); @@ -87,6 +70,7 @@ export const Exponent = (globalConfig: ExponentConfig): ExecutePlugin => { exponent: number ) => { const base = input[inputParameter]; + return Math.pow(base, exponent); }; diff --git a/src/builtins/exponent/types.ts b/src/builtins/exponent/types.ts deleted file mode 100644 index 54e8b2efc..000000000 --- a/src/builtins/exponent/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ExponentConfig = { - 'input-parameter': string; - exponent: number; - 'output-parameter': string; -}; diff --git a/src/builtins/export-csv-raw.ts b/src/builtins/export-csv-raw.ts index 223680759..7afc21cb2 100644 --- a/src/builtins/export-csv-raw.ts +++ b/src/builtins/export-csv-raw.ts @@ -1,16 +1,14 @@ import * as fs from 'fs/promises'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../util/errors'; +import {STRINGS} from '../config'; -import {ExhaustPluginInterface} from '../types/exhaust-plugin-interface'; import {Context} from '../types/manifest'; -import {STRINGS} from '../config/strings'; +const {ExhaustOutputArgError, WriteFileError} = ERRORS; +const {OUTPUT_REQUIRED, WRITE_CSV_ERROR, EXPORTING_RAW_CSV_FILE} = STRINGS; -const {ExhaustError} = ERRORS; -const {EXPORTING_RAW_CSV_FILE} = STRINGS; - -export const ExportCSVRaw = (): ExhaustPluginInterface => { +export const ExportCSVRaw = () => { /** * handle a tree leaf, where there are no child nodes, by adding it as key->value pair to the flat map * and capturing key as a header @@ -130,7 +128,7 @@ export const ExportCSVRaw = (): ExhaustPluginInterface => { try { await fs.writeFile(`${outputPath}.csv`, content); } catch (error) { - throw new ExhaustError(`Failed to write CSV to ${outputPath}: ${error}`); + throw new WriteFileError(WRITE_CSV_ERROR(outputPath, error)); } }; @@ -139,7 +137,7 @@ export const ExportCSVRaw = (): ExhaustPluginInterface => { */ const execute = async (tree: any, _context: Context, outputPath: string) => { if (!outputPath) { - throw new ExhaustError('Output path is required.'); + throw new ExhaustOutputArgError(OUTPUT_REQUIRED); } console.debug(EXPORTING_RAW_CSV_FILE(outputPath)); diff --git a/src/builtins/export-csv.ts b/src/builtins/export-csv.ts index 7accbf932..53a105733 100644 --- a/src/builtins/export-csv.ts +++ b/src/builtins/export-csv.ts @@ -1,15 +1,15 @@ import {writeFile} from 'fs/promises'; + import {stringify} from 'csv-stringify/sync'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {PluginParams} from '@grnsft/if-core/types'; -import {ERRORS} from '../util/errors'; +import {STRINGS} from '../config'; import {Context} from '../types/manifest'; -import {PluginParams} from '../types/interface'; - -import {STRINGS} from '../config/strings'; -const {ExhaustError} = ERRORS; -const {EXPORTING_TO_CSV_FILE} = STRINGS; +const {ExhaustOutputArgError} = ERRORS; +const {CSV_EXPORT, OUTPUT_REQUIRED, EXPORTING_TO_CSV_FILE} = STRINGS; /** * Extension to IF that outputs the tree in a CSV format. @@ -19,15 +19,13 @@ export const ExportCSV = () => { const validatedPath = validateOutputPath(outputPath); const paths = validatedPath.split('#'); - const output = paths.slice(0, paths.length - 1).join(''); const criteria = paths[paths.length - 1]; if (paths.length <= 1 || !criteria) { - throw new ExhaustError( - 'CSV export criteria is not found in output path. Please append it after --output #.' - ); + throw new ExhaustOutputArgError(CSV_EXPORT); } + const output = paths.slice(0, paths.length - 1).join(''); console.debug(EXPORTING_TO_CSV_FILE(output)); return { @@ -41,11 +39,7 @@ export const ExportCSV = () => { */ const validateOutputPath = (outputPath: string) => { if (!outputPath) { - throw new ExhaustError('Output path is required.'); - } - - if (!outputPath.includes('#')) { - throw new ExhaustError('Output path should contain `#`.'); + throw new ExhaustOutputArgError(OUTPUT_REQUIRED); } return outputPath; diff --git a/src/builtins/export-log.ts b/src/builtins/export-log.ts index 3120f3386..fee267dc2 100644 --- a/src/builtins/export-log.ts +++ b/src/builtins/export-log.ts @@ -1,4 +1,5 @@ import * as YAML from 'js-yaml'; + import {Context} from '../types/manifest'; export const ExportLog = () => { diff --git a/src/builtins/export-yaml.ts b/src/builtins/export-yaml.ts index 99bb35f5a..624e1298f 100644 --- a/src/builtins/export-yaml.ts +++ b/src/builtins/export-yaml.ts @@ -1,12 +1,13 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + import {saveYamlFileAs} from '../util/yaml'; -import {ERRORS} from '../util/errors'; -import {Context} from '../types/manifest'; +import {STRINGS} from '../config'; -import {STRINGS} from '../config/strings'; +import {Context} from '../types/manifest'; -const {ExhaustError} = ERRORS; -const {EXPORTING_TO_YAML_FILE} = STRINGS; +const {ExhaustOutputArgError} = ERRORS; +const {OUTPUT_REQUIRED, EXPORTING_TO_YAML_FILE} = STRINGS; export const ExportYaml = () => { /** Takes string before hashtag. */ @@ -17,7 +18,7 @@ export const ExportYaml = () => { */ const execute = async (tree: any, context: Context, outputPath: string) => { if (!outputPath) { - throw new ExhaustError('Output path is required.'); + throw new ExhaustOutputArgError(OUTPUT_REQUIRED); } const outputFile = { diff --git a/src/builtins/group-by.ts b/src/builtins/group-by.ts index 6a7036adf..63c4cd5c5 100644 --- a/src/builtins/group-by.ts +++ b/src/builtins/group-by.ts @@ -1,16 +1,18 @@ import {z} from 'zod'; +import {ERRORS} from '@grnsft/if-core/utils'; +import { + GroupByPlugin, + PluginParams, + GroupByConfig, +} from '@grnsft/if-core/types'; import {STRINGS} from '../config'; -import {GroupByPlugin, PluginParams} from '../types/interface'; -import {GroupByConfig} from '../types/group-by'; - -import {ERRORS} from '../util/errors'; import {validate} from '../util/validations'; -const {InvalidGroupingError, InputValidationError} = ERRORS; +const {InvalidGroupingError, GlobalConfigError} = ERRORS; -const {INVALID_GROUP_BY} = STRINGS; +const {INVALID_GROUP_BY, MISSING_GLOBAL_CONFIG} = STRINGS; /** * Plugin for inputs grouping. @@ -79,7 +81,7 @@ export const GroupBy = (): GroupByPlugin => { */ const validateConfig = (config: GroupByConfig) => { if (!config) { - throw new InputValidationError('Config is not provided.'); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } const schema = z.object({ diff --git a/src/builtins/index.ts b/src/builtins/index.ts index 1a98e3273..d22491aca 100644 --- a/src/builtins/index.ts +++ b/src/builtins/index.ts @@ -13,3 +13,4 @@ export {Exponent} from './exponent'; export {CSVLookup} from './csv-lookup'; export {Shell} from './shell'; export {Regex} from './regex'; +export {Copy} from './copy-param'; diff --git a/src/builtins/interpolation/README.md b/src/builtins/interpolation/README.md index cfcca2fa1..498c26c6e 100644 --- a/src/builtins/interpolation/README.md +++ b/src/builtins/interpolation/README.md @@ -164,5 +164,33 @@ You can execute this by passing it to `ie`. Run the impact using the following c ```sh npm i -g @grnsft/if -ie --manifest ./manifests/examples/interpolation.yml --output ./manifests/outputs/interpolation.yml +if-run --manifest ./manifests/examples/interpolation.yml --output ./manifests/outputs/interpolation.yml ``` + +## Errors + +`Interpolation` exposes one of IF's error classes. + +## `GlobalConfigError` + +### GlobalConfigError + +You will receive an error starting `GlobalConfigError: ` if you have not provided the expected configuration data in the plugin's `initialize` block. + +The required parameters are: +- `method`: a string containing either `linear`, `spline` or `polynomial` +- `x`: an array of numbers +- `y`: an array of numbers +- `input-parameter`: a string containing the name of a value present in the `inputs` array' +- `output-parameter`: a string + +You can fix this error by checking you are providing valid values for each parameter in the config. + +### Validation errors + +There are also several validation errors that can arise, including: +- if the lengths of `x` and `y` are not equal +- if `x` or `y` are empty +- if the requested point to interpolate at is outside the range of `x` + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/interpolation/index.ts b/src/builtins/interpolation/index.ts index 3c0e0381b..98a84ad8a 100644 --- a/src/builtins/interpolation/index.ts +++ b/src/builtins/interpolation/index.ts @@ -1,14 +1,24 @@ import Spline from 'typescript-cubic-spline'; import {z} from 'zod'; - -import {ExecutePlugin, PluginParams, ConfigParams} from '../../types/interface'; +import {ERRORS} from '@grnsft/if-core/utils'; +import { + ExecutePlugin, + PluginParams, + ConfigParams, + Method, +} from '@grnsft/if-core/types'; import {validate} from '../../util/validations'; -import {ERRORS} from '../../util/errors'; -import {Method} from './types'; +import {STRINGS} from '../../config'; -const {ConfigNotFoundError} = ERRORS; +const {GlobalConfigError} = ERRORS; +const { + MISSING_GLOBAL_CONFIG, + X_Y_EQUAL, + ARRAY_LENGTH_NON_EMPTY, + WITHIN_THE_RANGE, +} = STRINGS; export const Interpolation = (globalConfig: ConfigParams): ExecutePlugin => { /** @@ -118,7 +128,7 @@ export const Interpolation = (globalConfig: ConfigParams): ExecutePlugin => { */ const validateConfig = () => { if (!globalConfig) { - throw new ConfigNotFoundError('Global config is not provided.'); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } const schema = z @@ -130,10 +140,10 @@ export const Interpolation = (globalConfig: ConfigParams): ExecutePlugin => { 'output-parameter': z.string(), }) .refine(data => data.x && data.y && data.x.length === data.y.length, { - message: 'The length of `x` and `y` should be equal', + message: X_Y_EQUAL, }) .refine(data => data.x.length > 1 && data.y.length > 1, { - message: 'the length of the input arrays must be greater than 1', + message: ARRAY_LENGTH_NON_EMPTY, }); const defaultMethod = globalConfig.method ?? Method.LINEAR; @@ -171,8 +181,7 @@ export const Interpolation = (globalConfig: ConfigParams): ExecutePlugin => { data[inputParameter] >= globalConfig.x[0] && data[inputParameter] <= globalConfig.x[globalConfig.x.length - 1], { - message: - 'The target x value must be within the range of the given x values', + message: WITHIN_THE_RANGE, } ); diff --git a/src/builtins/interpolation/types.ts b/src/builtins/interpolation/types.ts deleted file mode 100644 index 09b7abb5e..000000000 --- a/src/builtins/interpolation/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum Method { - LINEAR = 'linear', - SPLINE = 'spline', - POLYNOMIAL = 'polynomial', -} diff --git a/src/builtins/mock-observations/README.md b/src/builtins/mock-observations/README.md index 629e27f2d..1af4cd6d6 100644 --- a/src/builtins/mock-observations/README.md +++ b/src/builtins/mock-observations/README.md @@ -91,7 +91,7 @@ You can run this example `manifest` by saving it as `manifests/plugins/mock-obse ```sh npm i -g @grnsft/if -ie --manifest ./examples/manifests/test/mock-observation.yml --output ./examples/outputs/mock-observation +if-run --manifest ./examples/manifests/test/mock-observation.yml --output ./examples/outputs/mock-observation ``` The results will be saved to a new `yaml` file in `./examples/outputs`. diff --git a/src/builtins/mock-observations/helpers/common-generator.ts b/src/builtins/mock-observations/helpers/common-generator.ts index 24bea4c94..8283cf1bd 100644 --- a/src/builtins/mock-observations/helpers/common-generator.ts +++ b/src/builtins/mock-observations/helpers/common-generator.ts @@ -1,37 +1,31 @@ -import {KeyValuePair} from '../../../types/common'; -import {ERRORS} from '../../../util/errors'; -import {buildErrorMessage} from '../../../util/helpers'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ConfigParams} from '@grnsft/if-core/types'; -import {Generator} from '../interfaces'; - -const {InputValidationError} = ERRORS; +import {STRINGS} from '../../../config'; -export const CommonGenerator = (config: KeyValuePair): Generator => { - const errorBuilder = buildErrorMessage(CommonGenerator.name); +import {Generator} from '../interfaces'; - /** - * Creates new copy of the given `object`. - */ - const copyObject = (object: T): T => ({...object}); +const {GlobalConfigError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; +export const CommonGenerator = (config: ConfigParams): Generator => { /** + * Generates next value by copying the validated config. * Validates the provided config is not null or empty. - * returns a copy of the validated config, otherwise throws an InputValidationError. + * Returns a copy of the validated config, otherwise throws an GlobalConfigError. */ const validateConfig = (config: object) => { if (!config || Object.keys(config).length === 0) { - throw new InputValidationError( - errorBuilder({message: 'Config must not be null or empty'}) - ); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } - return copyObject(config); + return structuredClone(config); }; /** * Generates next value by copying the validated config. */ - const next = (): Object => copyObject(validateConfig(config)); + const next = (): Object => validateConfig(config); return { next, diff --git a/src/builtins/mock-observations/helpers/rand-int-generator.ts b/src/builtins/mock-observations/helpers/rand-int-generator.ts index e27d694c3..1cb4d161f 100644 --- a/src/builtins/mock-observations/helpers/rand-int-generator.ts +++ b/src/builtins/mock-observations/helpers/rand-int-generator.ts @@ -1,70 +1,55 @@ -import {KeyValuePair} from '../../../types/common'; -import {ERRORS} from '../../../util/errors'; -import {buildErrorMessage} from '../../../util/helpers'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {RandIntGeneratorParams, ConfigParams} from '@grnsft/if-core/types'; + +import {STRINGS} from '../../../config'; import {Generator} from '../interfaces'; -import {RandIntGeneratorParams} from '../types'; -const {InputValidationError} = ERRORS; +const {GlobalConfigError} = ERRORS; + +const {MISSING_GLOBAL_CONFIG, MISSING_MIN_MAX, INVALID_MIN_MAX, INVALID_NAME} = + STRINGS; export const RandIntGenerator = ( name: string, - config: KeyValuePair + config: ConfigParams ): Generator => { - const errorBuilder = buildErrorMessage(RandIntGenerator.name); - const next = () => ({ [validatedName]: generateRandInt(getFieldToPopulate()), }); const validateName = (name: string | null): string => { if (!name || name.trim() === '') { - throw new InputValidationError( - errorBuilder({ - message: '`name` is empty or all spaces', - }) - ); + throw new GlobalConfigError(INVALID_NAME); } + return name; }; - const validateConfig = (config: KeyValuePair): {min: number; max: number} => { + const validateConfig = (config: ConfigParams): {min: number; max: number} => { if (!config || Object.keys(config).length === 0) { - throw new InputValidationError( - errorBuilder({ - message: 'Config must not be null or empty', - }) - ); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } if (!config.min || !config.max) { - throw new InputValidationError( - errorBuilder({ - message: 'Config is missing min or max', - }) - ); + throw new GlobalConfigError(MISSING_MIN_MAX); } if (config.min >= config.max) { - throw new InputValidationError( - errorBuilder({ - message: `Min value should not be greater than or equal to max value of ${validatedName}`, - }) - ); + throw new GlobalConfigError(INVALID_MIN_MAX(validatedName)); } + return {min: config.min, max: config.max}; }; const validatedName = validateName(name); const validatedConfig = validateConfig(config); - const getFieldToPopulate = () => { - return { - name: validatedName, - min: validatedConfig.min, - max: validatedConfig.max, - }; - }; + const getFieldToPopulate = () => ({ + name: validatedName, + min: validatedConfig.min, + max: validatedConfig.max, + }); const generateRandInt = ( randIntGenerator: RandIntGeneratorParams @@ -73,6 +58,7 @@ export const RandIntGenerator = ( const scaledNumber = randomNumber * (randIntGenerator.max - randIntGenerator.min) + randIntGenerator.min; + return Math.trunc(scaledNumber); }; diff --git a/src/builtins/mock-observations/index.ts b/src/builtins/mock-observations/index.ts index 2d65f4643..c7488de0f 100644 --- a/src/builtins/mock-observations/index.ts +++ b/src/builtins/mock-observations/index.ts @@ -1,15 +1,18 @@ import {DateTime, Duration} from 'luxon'; import {z} from 'zod'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {ConfigParams, KeyValuePair} from '../../types/common'; +import { + ExecutePlugin, + PluginParams, + ConfigParams, + ObservationParams, +} from '@grnsft/if-core/types'; import {validate} from '../../util/validations'; import {CommonGenerator} from './helpers/common-generator'; import {RandIntGenerator} from './helpers/rand-int-generator'; + import {Generator} from './interfaces/index'; -import {ObservationParams} from './types'; export const MockObservations = (globalConfig: ConfigParams): ExecutePlugin => { const metadata = { @@ -122,11 +125,10 @@ export const MockObservations = (globalConfig: ConfigParams): ExecutePlugin => { CommonGenerator(config), ]; - const createRandIntGenerators = (config: any): Generator[] => { - return Object.entries(config).map(([fieldToPopulate, value]) => - RandIntGenerator(fieldToPopulate, value as KeyValuePair) + const createRandIntGenerators = (config: any): Generator[] => + Object.entries(config).map(([fieldToPopulate, value]) => + RandIntGenerator(fieldToPopulate, value as Record) ); - }; return Object.entries(generatorsConfig).flatMap(([key, value]) => key === 'randint' diff --git a/src/builtins/mock-observations/types.ts b/src/builtins/mock-observations/types.ts deleted file mode 100644 index 82591207c..000000000 --- a/src/builtins/mock-observations/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {DateTime} from 'luxon'; - -import {Generator} from './interfaces/index'; - -export type ObservationParams = { - duration: number; - timeBucket: DateTime; - component: Record; - generators: Generator[]; -}; - -export type RandIntGeneratorParams = { - name: string; - min: number; - max: number; -}; diff --git a/src/builtins/multiply/README.md b/src/builtins/multiply/README.md index d4e27674c..7e9c2dfbd 100644 --- a/src/builtins/multiply/README.md +++ b/src/builtins/multiply/README.md @@ -88,7 +88,22 @@ You can run this example by saving it as `./examples/manifests/test/multiply.yml ```sh npm i -g @grnsft/if -ie --manifest ./examples/manifests/test/multiply.yml --output ./examples/outputs/multiply.yml +if-run --manifest ./examples/manifests/test/multiply.yml --output ./examples/outputs/multiply.yml ``` The results will be saved to a new `yaml` file in `./examples/outputs` + + +## Errors + +`Multiply` uses one of the IF error classes. + +### `MissingInputDataError` + +This error arises when a necessary piece of input data is missing from the `inputs` array. +Every element in the `inputs` array must contain: +- `timestamp` +- `duration` +- whatever values you passed to `input-parameters` + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/multiply/index.ts b/src/builtins/multiply/index.ts index 5666a6093..cd2a74837 100644 --- a/src/builtins/multiply/index.ts +++ b/src/builtins/multiply/index.ts @@ -1,16 +1,13 @@ import {z} from 'zod'; +import { + ExecutePlugin, + PluginParams, + MultiplyConfig, +} from '@grnsft/if-core/types'; -import {buildErrorMessage} from '../../util/helpers'; -import {ERRORS} from '../../util/errors'; import {validate} from '../../util/validations'; -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {MultiplyConfig} from './types'; - -const {InputValidationError} = ERRORS; - export const Multiply = (globalConfig: MultiplyConfig): ExecutePlugin => { - const errorBuilder = buildErrorMessage(Multiply.name); const metadata = { kind: 'execute', }; @@ -37,18 +34,18 @@ export const Multiply = (globalConfig: MultiplyConfig): ExecutePlugin => { input: PluginParams, inputParameters: string[] ) => { - inputParameters.forEach(metricToMultiply => { - if ( - input[metricToMultiply] === undefined || - isNaN(input[metricToMultiply]) - ) { - throw new InputValidationError( - errorBuilder({ - message: `${metricToMultiply} is missing from the input array`, - }) - ); - } - }); + const inputData = inputParameters.reduce( + (acc, param) => { + acc[param] = input[param]; + + return acc; + }, + {} as Record + ); + + const validationSchema = z.record(z.string(), z.number()); + + validate(validationSchema, inputData); return input; }; @@ -62,17 +59,17 @@ export const Multiply = (globalConfig: MultiplyConfig): ExecutePlugin => { const outputParameter = safeGlobalConfig['output-parameter']; return inputs.map(input => { - const safeInput = validateSingleInput(input, inputParameters); + validateSingleInput(input, inputParameters); return { ...input, - [outputParameter]: calculateProduct(safeInput, inputParameters), + [outputParameter]: calculateProduct(input, inputParameters), }; }); }; /** - * Calculates the product of the energy components. + * Calculates the product of the components. */ const calculateProduct = (input: PluginParams, inputParameters: string[]) => inputParameters.reduce( diff --git a/src/builtins/multiply/types.ts b/src/builtins/multiply/types.ts deleted file mode 100644 index 88f4cae49..000000000 --- a/src/builtins/multiply/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type MultiplyConfig = { - 'input-parameters': string[]; - 'output-parameter': string; -}; diff --git a/src/builtins/regex/README.md b/src/builtins/regex/README.md index 52a99db9f..a0fa5d624 100644 --- a/src/builtins/regex/README.md +++ b/src/builtins/regex/README.md @@ -4,8 +4,6 @@ You provide the name of the value you want to match, and a name to use to add the regex to the output array. -For example, `boavizta-cpu` need `cpu/name` to work, however `cloud-metadata` returns `physical-processor` which usually contains a long string of processors that the instance could be separated by `,`, like so: - ``` Intel® Xeon® Platinum 8272CL,Intel® Xeon® 8171M 2.1 GHz,Intel® Xeon® E5-2673 v4 2.3 GHz,Intel® Xeon® E5-2673 v3 2.4 GHz ``` @@ -89,3 +87,35 @@ if --manifest manifests/examples/regex.yml --output manifests/outputs/regex.yml ``` The results will be saved to a new `yaml` file in `manifests/outputs`. + +## Errors + +`Regex` uses three of IF's error classes: + +### `MissingInputDataError` + +This error arises when a necessary piece of input data is missing from the `inputs` array. +Every element in the `inputs` array must contain: +- `timestamp` +- `duration` +- whatever value you passed to `parameter` + + +### `GlobalConfigError` + +You will receive an error starting `GlobalConfigError: ` if you have not provided the expected configuration data in the plugin's `initialize` block. + +The required parameters are: + +- `parameter`: a string containing the name of a value in the inputs array +- `match`: a valid regex pattern +- `output`: a string + +You can fix this error by checking you are providing valid values for each parameter in the config. + + +### `RegexMismatchError` + +This error arises when the requested regex cannot find any matches in the given data. If there are multiple matches, the plugin returns the first, but if there are none, it throws this error. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/regex/index.ts b/src/builtins/regex/index.ts index 7dc854461..8d382ec74 100644 --- a/src/builtins/regex/index.ts +++ b/src/builtins/regex/index.ts @@ -1,16 +1,15 @@ import {z} from 'zod'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams, ConfigParams} from '@grnsft/if-core/types'; -import {buildErrorMessage} from '../../util/helpers'; -import {ERRORS} from '../../util/errors'; import {validate} from '../../util/validations'; -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {ConfigParams} from '../../types/common'; +import {STRINGS} from '../../config'; -const {InputValidationError, ConfigValidationError} = ERRORS; +const {MissingInputDataError, GlobalConfigError, RegexMismatchError} = ERRORS; +const {MISSING_GLOBAL_CONFIG, MISSING_INPUT_DATA, REGEX_MISMATCH} = STRINGS; export const Regex = (globalConfig: ConfigParams): ExecutePlugin => { - const errorBuilder = buildErrorMessage(Regex.name); const metadata = { kind: 'execute', }; @@ -20,10 +19,9 @@ export const Regex = (globalConfig: ConfigParams): ExecutePlugin => { */ const validateGlobalConfig = () => { if (!globalConfig) { - throw new ConfigValidationError( - errorBuilder({message: 'Configuration data is missing'}) - ); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } + const schema = z.object({ parameter: z.string().min(1), match: z.string().min(1), @@ -38,11 +36,7 @@ export const Regex = (globalConfig: ConfigParams): ExecutePlugin => { */ const validateSingleInput = (input: PluginParams, parameter: string) => { if (!input[parameter]) { - throw new InputValidationError( - errorBuilder({ - message: `\`${parameter}\` is missing from the input`, - }) - ); + throw new MissingInputDataError(MISSING_INPUT_DATA(parameter)); } return input; @@ -89,11 +83,7 @@ export const Regex = (globalConfig: ConfigParams): ExecutePlugin => { const matchedItem = input[parameter].match(regex); if (!matchedItem || !matchedItem[0]) { - throw new InputValidationError( - errorBuilder({ - message: `\`${input[parameter]}\` does not match the ${match} regex expression`, - }) - ); + throw new RegexMismatchError(REGEX_MISMATCH(input[parameter], match)); } return matchedItem[0]; diff --git a/src/builtins/sci-embodied/README.md b/src/builtins/sci-embodied/README.md index 90fc01d58..97b6ce4af 100644 --- a/src/builtins/sci-embodied/README.md +++ b/src/builtins/sci-embodied/README.md @@ -104,7 +104,21 @@ You can run this example `manifest` by executing the following command from the ```sh npm i -g @grnsft/if -ie --manifest manifests/plugins/sci-embodied.yml --output manifests/outputs/sci-embodied.yml +if-run --manifest manifests/plugins/sci-embodied.yml --output manifests/outputs/sci-embodied.yml ``` The results will be saved to a new `yaml` file in `./examples/outputs`. + + +## Errors + +`SciEmbodied` uses one of IF's error classes + +### `SciEmbodiedError` + +This error class is used to describe a problem with one of the input values to `sci-embodied`. This is typically due to an incorrect type or a reference to a value that is not available. + +You will receive a specific error message explaining which parameter is problematic, and you can check and replace where appropriate. + + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/sci-embodied/index.ts b/src/builtins/sci-embodied/index.ts index 162099860..a88017e9f 100644 --- a/src/builtins/sci-embodied/index.ts +++ b/src/builtins/sci-embodied/index.ts @@ -1,9 +1,12 @@ import {z} from 'zod'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; +import {ExecutePlugin, PluginParams} from '@grnsft/if-core/types'; import {validate, allDefined} from '../../util/validations'; +import {STRINGS} from '../../config'; + +const {SCI_EMBODIED_ERROR} = STRINGS; + export const SciEmbodied = (): ExecutePlugin => { const metadata = { kind: 'execute', @@ -21,8 +24,8 @@ export const SciEmbodied = (): ExecutePlugin => { /** * Calculate the Embodied carbon for a list of inputs. */ - const execute = (inputs: PluginParams[]) => { - return inputs.map(input => { + const execute = (inputs: PluginParams[]) => + inputs.map(input => { const safeInput = validateInput(input); return { @@ -30,7 +33,6 @@ export const SciEmbodied = (): ExecutePlugin => { 'carbon-embodied': calculateEmbodiedCarbon(safeInput), }; }); - }; /** * Calculate the Embodied carbon for the input. @@ -55,9 +57,6 @@ export const SciEmbodied = (): ExecutePlugin => { * Checks for required fields in input. */ const validateInput = (input: PluginParams) => { - const errorMessage = (unit: string) => - `not a valid number in input. Please provide it as \`${unit}\``; - const commonSchemaPart = (errorMessage: (unit: string) => string) => ({ 'device/emissions-embodied': z .number({ @@ -81,13 +80,13 @@ export const SciEmbodied = (): ExecutePlugin => { const vcpusSchemaPart = { 'vcpus-allocated': z .number({ - invalid_type_error: errorMessage('count'), + invalid_type_error: SCI_EMBODIED_ERROR('count'), }) .gte(0) .min(0), 'vcpus-total': z .number({ - invalid_type_error: errorMessage('count'), + invalid_type_error: SCI_EMBODIED_ERROR('count'), }) .gte(0) .min(0), @@ -96,24 +95,24 @@ export const SciEmbodied = (): ExecutePlugin => { const resourcesSchemaPart = { 'resources-reserved': z .number({ - invalid_type_error: errorMessage('count'), + invalid_type_error: SCI_EMBODIED_ERROR('count'), }) .gte(0) .min(0), 'resources-total': z .number({ - invalid_type_error: errorMessage('count'), + invalid_type_error: SCI_EMBODIED_ERROR('count'), }) .gte(0) .min(0), }; const schemaWithVcpus = z.object({ - ...commonSchemaPart(errorMessage), + ...commonSchemaPart(SCI_EMBODIED_ERROR), ...vcpusSchemaPart, }); const schemaWithResources = z.object({ - ...commonSchemaPart(errorMessage), + ...commonSchemaPart(SCI_EMBODIED_ERROR), ...resourcesSchemaPart, }); diff --git a/src/builtins/sci/README.md b/src/builtins/sci/README.md index 477f74dc0..592bd1ed0 100644 --- a/src/builtins/sci/README.md +++ b/src/builtins/sci/README.md @@ -19,6 +19,8 @@ - `sci`: carbon expressed in terms of the given functional unit +>Note: Plugin will warn and return `carbon` value in case if `functional-unit`'s value is zero. + ## Calculation SCI is calculated as: @@ -83,7 +85,28 @@ You can run this example `manifest` by saving it as `./manifests/plugins/sci.yml ```sh npm i -g @grnsft/if -ie --manifest manifests/plugins/sci.yml --output manifests/outputs/sci.yml +if-run --manifest manifests/plugins/sci.yml --output manifests/outputs/sci.yml ``` The results will be saved to a new `yaml` file. + +## Errors + +`SCI` uses one of the IF error classes. + +### `MissingInputDataError` + +This error arises when a necessary piece of input data is missing from the `inputs` array. + +Every element in the `inputs` array must contain: +- `timestamp` +- `duration` +- `carbon`: a numeric value named `carbon` must exist in the inputs array +- whatever value you passed to `functional-unit` + + +### Validation errors + +There is also a validation step that checks that the `functional-unit` was provided in the plugin config. If you see an error reporting this value as missing, please check you have provided it. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/sci/index.ts b/src/builtins/sci/index.ts index 7a60db439..47d495351 100644 --- a/src/builtins/sci/index.ts +++ b/src/builtins/sci/index.ts @@ -1,16 +1,20 @@ import {z} from 'zod'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {ConfigParams} from '../../types/common'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams, ConfigParams} from '@grnsft/if-core/types'; import {validate, allDefined} from '../../util/validations'; -import {buildErrorMessage} from '../../util/helpers'; -import {ERRORS} from '../../util/errors'; -const {InputValidationError} = ERRORS; +import {STRINGS} from '../../config'; + +const {MissingInputDataError} = ERRORS; +const { + MISSING_FUNCTIONAL_UNIT_CONFIG, + MISSING_FUNCTIONAL_UNIT_INPUT, + SCI_MISSING_FN_UNIT, + ZERO_DIVISION, +} = STRINGS; export const Sci = (globalConfig: ConfigParams): ExecutePlugin => { - const errorBuilder = buildErrorMessage(Sci.name); const metadata = { kind: 'execute', }; @@ -19,15 +23,12 @@ export const Sci = (globalConfig: ConfigParams): ExecutePlugin => { * Validates node and gloabl configs. */ const validateConfig = (config?: ConfigParams) => { - const errorMessage = - '`functional-unit` should be provided in your global config'; - const schema = z .object({ 'functional-unit': z.string(), }) .refine(data => data['functional-unit'], { - message: errorMessage, + message: MISSING_FUNCTIONAL_UNIT_CONFIG, }); return validate>(schema, config); @@ -36,40 +37,39 @@ export const Sci = (globalConfig: ConfigParams): ExecutePlugin => { /** * Calculate the total emissions for a list of inputs. */ - const execute = (inputs: PluginParams[]): PluginParams[] => { - return inputs.map(input => { + const execute = (inputs: PluginParams[]): PluginParams[] => + inputs.map((input, index) => { const safeInput = validateInput(input); - const sci = - safeInput['carbon'] > 0 - ? safeInput['carbon'] / input[globalConfig['functional-unit']] - : 0; + const functionalUnit = input[globalConfig['functional-unit']]; + + if (functionalUnit === 0) { + console.warn(ZERO_DIVISION(Sci.name, index)); + + return { + ...input, + sci: safeInput['carbon'], + }; + } + return { ...input, - sci, + sci: safeInput['carbon'] / functionalUnit, }; }); - }; /** * Checks for fields in input. */ const validateInput = (input: PluginParams) => { - const message = `'carbon' and ${globalConfig['functional-unit']} should be present in your input data.`; - const validatedConfig = validateConfig(globalConfig); if ( !( validatedConfig['functional-unit'] in input && - input[validatedConfig['functional-unit']] > 0 + input[validatedConfig['functional-unit']] >= 0 ) ) { - throw new InputValidationError( - errorBuilder({ - message: - 'functional-unit value is missing from input data or it is not a positive integer', - }) - ); + throw new MissingInputDataError(MISSING_FUNCTIONAL_UNIT_INPUT); } const schema = z @@ -77,7 +77,9 @@ export const Sci = (globalConfig: ConfigParams): ExecutePlugin => { carbon: z.number().gte(0), duration: z.number().gte(1), }) - .refine(allDefined, {message}); + .refine(allDefined, { + message: SCI_MISSING_FN_UNIT(globalConfig['functional-unit']), + }); return validate>(schema, input); }; diff --git a/src/builtins/shell/README.md b/src/builtins/shell/README.md index ea813878b..bacba1255 100644 --- a/src/builtins/shell/README.md +++ b/src/builtins/shell/README.md @@ -124,7 +124,18 @@ You can run this example `manifest` by saving it as `manifests/plugins/shell.yml ```sh npm i -g @grnsft/if -ie --manifest manifests/plugins/shell.yml --output manifests/outputs/shell.yml +if-run --manifest manifests/plugins/shell.yml --output manifests/outputs/shell.yml ``` The results will be saved to a new `yaml` file. + + +## Errors + +`Shell` uses one of the error classes provided by IF + +### `ProcessExecutionError` + +This error is thrown when the program invoked in the spawned shell fails for some reason that is not known to IF. Since the shell executes arbitrary code, it is difficult for IF to provide specific errors - this is delegated to the developers of the executed program. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/shell/index.ts b/src/builtins/shell/index.ts index 43b1c224c..352ee6bcd 100644 --- a/src/builtins/shell/index.ts +++ b/src/builtins/shell/index.ts @@ -1,14 +1,13 @@ import {spawnSync, SpawnSyncReturns} from 'child_process'; + import {loadAll, dump} from 'js-yaml'; import {z} from 'zod'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {ConfigParams} from '../../types/common'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams, ConfigParams} from '@grnsft/if-core/types'; import {validate} from '../../util/validations'; -import {ERRORS} from '../../util/errors'; -const {InputValidationError} = ERRORS; +const {ProcessExecutionError} = ERRORS; export const Shell = (globalConfig: ConfigParams): ExecutePlugin => { const metadata = { @@ -55,7 +54,7 @@ export const Shell = (globalConfig: ConfigParams): ExecutePlugin => { return {outputs}; } catch (error: any) { - throw new InputValidationError(error.message); + throw new ProcessExecutionError(error.message); } }; diff --git a/src/builtins/subtract/README.md b/src/builtins/subtract/README.md index 9279220b8..8ff9a922f 100644 --- a/src/builtins/subtract/README.md +++ b/src/builtins/subtract/README.md @@ -88,7 +88,18 @@ You can run this example by saving it as `./examples/manifests/test/subrtact.yml ```sh npm i -g @grnsft/if -ie --manifest /manifests/plugins/subtract.yml --output manifests/outputs/subtract.yml +if-run --manifest /manifests/plugins/subtract.yml --output manifests/outputs/subtract.yml ``` The results will be saved to a new `yaml` file in `manifests/outputs`. + + +## Errors + +`Subtract` uses one of IF's error classes: + +### `InputValidationError` + +This error arises when an invalid value is passed to `Subtract`. Typically, this can occur when a non-numeric value (such as a string made of alphabetic characters) is passed where a number or numeric string is expected. Please check that the types are correct for all the relevant fields in your `inputs` array. + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors diff --git a/src/builtins/subtract/index.ts b/src/builtins/subtract/index.ts index 0af02898a..797b259c1 100644 --- a/src/builtins/subtract/index.ts +++ b/src/builtins/subtract/index.ts @@ -1,16 +1,13 @@ import {z} from 'zod'; +import { + ExecutePlugin, + PluginParams, + SubtractConfig, +} from '@grnsft/if-core/types'; -import {ERRORS} from '../../util/errors'; -import {buildErrorMessage} from '../../util/helpers'; import {validate} from '../../util/validations'; -import {ExecutePlugin, PluginParams} from '../../types/interface'; -import {SubtractConfig} from './types'; - -const {InputValidationError} = ERRORS; - export const Subtract = (globalConfig: SubtractConfig): ExecutePlugin => { - const errorBuilder = buildErrorMessage(Subtract.name); const metadata = { kind: 'execute', }; @@ -37,32 +34,20 @@ export const Subtract = (globalConfig: SubtractConfig): ExecutePlugin => { input: PluginParams, inputParameters: string[] ) => { - inputParameters.forEach(metricToSubtract => { - validateParamExists(input, metricToSubtract); - validateNumericString(input[metricToSubtract]); - }); + const inputData = inputParameters.reduce( + (acc, param) => { + acc[param] = input[param]; - return input; - }; + return acc; + }, + {} as Record + ); - const validateParamExists = (input: PluginParams, param: string) => { - if (input[param] === undefined) { - throw new InputValidationError( - errorBuilder({ - message: `${param} is missing from the input array`, - }) - ); - } - }; + const validationSchema = z.record(z.string(), z.number()); + + validate(validationSchema, inputData); - const validateNumericString = (str: string) => { - if (isNaN(+Number(str))) { - throw new InputValidationError( - errorBuilder({ - message: `${str} is not numberic`, - }) - ); - } + return input; }; /** @@ -73,6 +58,7 @@ export const Subtract = (globalConfig: SubtractConfig): ExecutePlugin => { 'input-parameters': inputParameters, 'output-parameter': outputParameter, } = validateGlobalConfig(); + return inputs.map(input => { validateSingleInput(input, inputParameters); @@ -88,6 +74,7 @@ export const Subtract = (globalConfig: SubtractConfig): ExecutePlugin => { */ const calculateDiff = (input: PluginParams, inputParameters: string[]) => { const [firstItem, ...restItems] = inputParameters; + return restItems.reduce( (accumulator, metricToSubtract) => accumulator - input[metricToSubtract], input[firstItem] // Starting accumulator with the value of the first item diff --git a/src/builtins/subtract/types.ts b/src/builtins/subtract/types.ts deleted file mode 100644 index 4dc6775d2..000000000 --- a/src/builtins/subtract/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type SubtractConfig = { - 'input-parameters': string[]; - 'output-parameter': string; -}; diff --git a/src/builtins/sum/README.md b/src/builtins/sum/README.md index b912c07b3..ea0f0113d 100644 --- a/src/builtins/sum/README.md +++ b/src/builtins/sum/README.md @@ -85,7 +85,34 @@ tree: You can run this example by saving it as `./examples/manifests/sum.yml` and executing the following command from the project root: ```sh -ie --manifest ./examples/manifests/sum.yml --output ./examples/outputs/sum.yml +if-run --manifest ./examples/manifests/sum.yml --output ./examples/outputs/sum.yml ``` The results will be saved to a new `yaml` file in `./examples/outputs`. + + +## Errors + +`Sum` exposes two of the IF error classes. + +### GlobalConfigError + +You will receive an error starting `GlobalConfigError: ` if you have not provided the expected configuration data in the plugin's `initialize` block. + +The required parameters are: +- `input-parameters`: this must be an array of strings, each being the name of a value in the `inputs` array +- `output-parameter`: this must be a string + +You can fix this error by checking you are providing valid values for each parameter in the config. + +### `MissingInputDataError` + +This error arises when a necessary piece of input data is missing from the `inputs` array. +Every element in the ``inputs` array must contain: + +- `timestamp` +- `duration` +- whatever values you passed to `input-parameters` + + +For more information on our error classes, please visit [our docs](https://if.greensoftware.foundation/reference/errors). diff --git a/src/builtins/sum/index.ts b/src/builtins/sum/index.ts index 8eec61c37..57437ba31 100644 --- a/src/builtins/sum/index.ts +++ b/src/builtins/sum/index.ts @@ -1,13 +1,13 @@ import {z} from 'zod'; - -import {ExecutePlugin, PluginParams} from '../../types/interface'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {ExecutePlugin, PluginParams, SumConfig} from '@grnsft/if-core/types'; import {validate} from '../../util/validations'; -import {ERRORS} from '../../util/errors'; -import {SumConfig} from './types'; +import {STRINGS} from '../../config'; -const {InputValidationError, ConfigNotFoundError} = ERRORS; +const {GlobalConfigError} = ERRORS; +const {MISSING_GLOBAL_CONFIG} = STRINGS; export const Sum = (globalConfig: SumConfig): ExecutePlugin => { const metadata = { @@ -23,11 +23,11 @@ export const Sum = (globalConfig: SumConfig): ExecutePlugin => { const outputParameter = safeGlobalConfig['output-parameter']; return inputs.map(input => { - const safeInput = validateSingleInput(input, inputParameters); + validateSingleInput(input, inputParameters); return { ...input, - [outputParameter]: calculateSum(safeInput, inputParameters), + [outputParameter]: calculateSum(input, inputParameters), }; }); }; @@ -37,7 +37,7 @@ export const Sum = (globalConfig: SumConfig): ExecutePlugin => { */ const validateGlobalConfig = () => { if (!globalConfig) { - throw new ConfigNotFoundError('Global config is not provided.'); + throw new GlobalConfigError(MISSING_GLOBAL_CONFIG); } const globalConfigSchema = z.object({ @@ -58,13 +58,16 @@ export const Sum = (globalConfig: SumConfig): ExecutePlugin => { input: PluginParams, inputParameters: string[] ) => { - inputParameters.forEach(metricToSum => { - if (!input[metricToSum]) { - throw new InputValidationError( - `${metricToSum} is missing from the input array.` - ); - } - }); + const inputData = inputParameters.reduce( + (acc, param) => { + acc[param] = input[param]; + + return acc; + }, + {} as Record + ); + const validationSchema = z.record(z.string(), z.number()); + validate(validationSchema, inputData); return input; }; diff --git a/src/builtins/sum/types.ts b/src/builtins/sum/types.ts deleted file mode 100644 index e30c990e5..000000000 --- a/src/builtins/sum/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type SumConfig = { - 'input-parameters': string[]; - 'output-parameter': string; -}; diff --git a/src/builtins/time-sync.ts b/src/builtins/time-sync.ts index 67d5c9079..8b37241b8 100644 --- a/src/builtins/time-sync.ts +++ b/src/builtins/time-sync.ts @@ -1,29 +1,39 @@ import {isDate} from 'node:util/types'; + import {Settings, DateTime, DateTimeMaybeValid, Interval} from 'luxon'; import {z} from 'zod'; - -import {parameterize} from '../lib/parameterize'; - -import {ERRORS} from '../util/errors'; - -import {STRINGS} from '../config'; - -import {ExecutePlugin, PluginParams} from '../types/interface'; +import {ERRORS} from '@grnsft/if-core/utils'; import { + ExecutePlugin, + PluginParams, PaddingReceipt, TimeNormalizerConfig, TimeParams, -} from '../types/time-sync'; +} from '@grnsft/if-core/types'; + +import {parameterize} from '../lib/parameterize'; + import {validate} from '../util/validations'; +import {STRINGS} from '../config'; + Settings.defaultZone = 'utc'; -const {InputValidationError} = ERRORS; +const { + GlobalConfigError, + InvalidDateInInputError, + InvalidPaddingError, + InvalidInputError, +} = ERRORS; const { INVALID_TIME_NORMALIZATION, INVALID_OBSERVATION_OVERLAP, AVOIDING_PADDING_BY_EDGES, + INVALID_DATE_TYPE, + START_LOWER_END, + TIMESTAMP_REQUIRED, + INVALID_DATETIME, } = STRINGS; export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { @@ -64,7 +74,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { seconds: previousInput.duration, }) > currentMoment ) { - throw new InputValidationError(INVALID_OBSERVATION_OVERLAP); + throw new InvalidInputError(INVALID_OBSERVATION_OVERLAP); } const compareableTime = previousInputTimestamp.plus({ @@ -106,19 +116,22 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { }; const parseDate = (date: Date | string) => { - if (!date) return DateTime.invalid('Invalid date'); + if (!date) { + return DateTime.invalid('Invalid date'); + } + // dates are passed to time-sync.ts both in ISO 8601 format // and as a Date object (from the deserialization of a YAML file) // if the YAML parser fails to identify as a date, it passes as a string if (isDate(date)) { return DateTime.fromJSDate(date); } + if (typeof date === 'string') { return DateTime.fromISO(date); } - throw new InputValidationError( - `Unexpected date datatype: ${typeof date}: ${date}` - ); + + throw new InvalidDateInInputError(INVALID_DATE_TYPE(date)); }; /** @@ -128,10 +141,10 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const schema = z.object({ timestamp: z .string({ - required_error: `required in input[${index}]`, + required_error: TIMESTAMP_REQUIRED(index), }) .datetime({ - message: `invalid datetime in input[${index}]`, + message: INVALID_DATETIME(index), }) .or(z.date()), duration: z.number(), @@ -145,7 +158,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { */ const validateGlobalConfig = () => { if (globalConfig === undefined) { - throw new InputValidationError(INVALID_TIME_NORMALIZATION); + throw new GlobalConfigError(INVALID_TIME_NORMALIZATION); } const schema = z @@ -156,7 +169,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { 'allow-padding': z.boolean(), }) .refine(data => data['start-time'] < data['end-time'], { - message: '`start-time` should be lower than `end-time`', + message: START_LOWER_END, }); return validate>(schema, globalConfig); @@ -176,8 +189,10 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { i: number ) => { const thisMoment = parseDate(currentRoundMoment).startOf('second'); + return thisMoment.plus({seconds: i}); }; + /** * Breaks down input per minimal time unit. */ @@ -259,8 +274,9 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const validatePadding = (pad: PaddingReceipt, params: TimeParams): void => { const {start, end} = pad; const isPaddingNeeded = start || end; + if (!params.allowPadding && isPaddingNeeded) { - throw new InputValidationError(AVOIDING_PADDING_BY_EDGES(start, end)); + throw new InvalidPaddingError(AVOIDING_PADDING_BY_EDGES(start, end)); } }; @@ -292,8 +308,8 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { * Iterates over given inputs frame, meanwhile checking if aggregation method is `sum`, then calculates it. * For methods is `avg` and `none` calculating average of the frame. */ - const resampleInputFrame = (inputsInTimeslot: PluginParams[]) => { - return inputsInTimeslot.reduce((acc, input, index, inputs) => { + const resampleInputFrame = (inputsInTimeslot: PluginParams[]) => + inputsInTimeslot.reduce((acc, input, index, inputs) => { const metrics = Object.keys(input); metrics.forEach(metric => { @@ -336,13 +352,12 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { return acc; }, {} as PluginParams); - }; /** * Takes each array frame with interval length, then aggregating them together as from units.yaml file. */ - const resampleInputs = (inputs: PluginParams[], params: TimeParams) => { - return inputs.reduce((acc: PluginParams[], _input, index, inputs) => { + const resampleInputs = (inputs: PluginParams[], params: TimeParams) => + inputs.reduce((acc: PluginParams[], _input, index, inputs) => { const frameStart = index * params.interval; const frameEnd = (index + 1) * params.interval; const inputsFrame = inputs.slice(frameStart, frameEnd); @@ -356,7 +371,6 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { return acc; }, [] as PluginParams[]); - }; /** * Pads zeroish inputs from the beginning or at the end of the inputs if needed. @@ -394,6 +408,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { ) ); } + return paddedArray; }; @@ -404,6 +419,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { ) => { const array: PluginParams[] = []; const dateRange = Interval.fromDateTimes(startDate, endDate); + for (const interval of dateRange.splitBy({second: 1})) { array.push( fillWithZeroishInput( @@ -415,6 +431,7 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { ) ); } + return array; }; @@ -424,8 +441,8 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { const trimInputsByGlobalTimeline = ( inputs: PluginParams[], params: TimeParams - ): PluginParams[] => { - return inputs.reduce((acc: PluginParams[], item) => { + ): PluginParams[] => + inputs.reduce((acc: PluginParams[], item) => { const {timestamp} = item; if ( @@ -437,7 +454,6 @@ export const TimeSync = (globalConfig: TimeNormalizerConfig): ExecutePlugin => { return acc; }, [] as PluginParams[]); - }; return {metadata, execute}; }; diff --git a/src/check.ts b/src/check.ts new file mode 100644 index 000000000..3d53bc516 --- /dev/null +++ b/src/check.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/* eslint-disable no-process-exit */ +import * as path from 'path'; + +import {logger} from './util/logger'; +import {logStdoutFailMessage} from './util/helpers'; +import {parseIfCheckArgs} from './util/args'; +import {getYamlFiles, removeFileIfExists} from './util/fs'; + +import {STRINGS} from './config'; +import {executeCommands} from './util/npm'; + +const {CHECKING, DIRECTORY_YAML_FILES_NOT_FOUND} = STRINGS; + +const IfCheck = async () => { + const commandArgs = await parseIfCheckArgs(); + + console.log(`${CHECKING}\n`); + + if (commandArgs.manifest) { + const manifest = commandArgs.manifest; + + try { + await executeCommands(manifest, false); + } catch (error: any) { + const fileName = path.basename(manifest); + const executedFile = manifest + .replace(fileName, `re-${fileName}`) + .replace('yml', 'yaml'); + const manifestDirPath = path.dirname(manifest); + + logStdoutFailMessage(error, fileName); + + await removeFileIfExists(`${manifestDirPath}/package.json`); + await removeFileIfExists(executedFile); + } + } else { + const directory = commandArgs.directory; + const files = await getYamlFiles(directory!); + + if (files.length === 0) { + console.log(DIRECTORY_YAML_FILES_NOT_FOUND); + process.exit(1); + } + + for await (const file of files) { + const fileName = path.basename(file); + console.log(fileName); + + try { + await executeCommands(file, true); + } catch (error: any) { + const fileName = path.basename(file); + const executedFile = file + .replace(fileName, `re-${fileName}`) + .replace('yml', 'yaml'); + + logStdoutFailMessage(error, fileName); + + await removeFileIfExists(executedFile); + } + } + } +}; + +IfCheck().catch(error => { + if (error instanceof Error) { + logger.error(error); + process.exit(2); + } +}); diff --git a/src/config/config.ts b/src/config/config.ts index 5df3f3439..b8f676924 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -2,7 +2,12 @@ import {ArgumentConfig, ParseOptions} from 'ts-command-line-args'; import {STRINGS} from './strings'; -import {IFDiffArgs, IEArgs} from '../types/process-args'; +import { + IFDiffArgs, + IEArgs, + IFEnvArgs, + IFCheckArgs, +} from '../types/process-args'; const {DISCLAIMER_MESSAGE} = STRINGS; @@ -83,6 +88,68 @@ export const CONFIG = { SUCCESS_MESSAGE: 'Files match!', FAILURE_MESSAGE: 'Files do not match!', }, + IF_ENV: { + ARGS: { + manifest: { + type: String, + optional: true, + alias: 'm', + description: '[path to the manifest file]', + }, + install: { + type: Boolean, + optional: true, + alias: 'i', + description: '[command to install package.json]', + }, + cwd: { + type: Boolean, + optional: true, + alias: 'c', + description: + '[command to generate the package.json in the command working directory]', + }, + } as ArgumentConfig, + HELP: { + helpArg: 'help', + headerContentSections: [ + {header: 'Impact Framework', content: 'IF-Env Helpful keywords:'}, + ], + footerContentSections: [ + {header: 'Green Software Foundation', content: DISCLAIMER_MESSAGE}, + ], + } as ParseOptions, + SUCCESS_MESSAGE: 'The environment is successfully setup!', + FAILURE_MESSAGE: 'Faied to create the environment!', + FAILURE_MESSAGE_TEMPLATE: + 'Faied to create the environment with the template manifest!', + FAILURE_MESSAGE_DEPENDENCIES: 'Manifest dependencies are not available!', + }, + IF_CHECK: { + ARGS: { + manifest: { + type: String, + optional: true, + alias: 'm', + description: '[path to the manifest file]', + }, + directory: { + type: String, + optional: true, + alias: 'd', + description: '[path to the manifests directory]', + }, + } as ArgumentConfig, + HELP: { + helpArg: 'help', + headerContentSections: [ + {header: 'Impact Framework', content: 'IF-Check Helpful keywords:'}, + ], + footerContentSections: [ + {header: 'Green Software Foundation', content: DISCLAIMER_MESSAGE}, + ], + } as ParseOptions, + }, GITHUB_PATH: 'https://github.com', NATIVE_PLUGIN: 'if-plugins', AGGREGATION_ADDITIONAL_PARAMS: ['timestamp', 'duration'], diff --git a/src/config/env-template.yml b/src/config/env-template.yml new file mode 100644 index 000000000..f5fd65a6a --- /dev/null +++ b/src/config/env-template.yml @@ -0,0 +1,24 @@ +name: template manifest # rename me! +description: auto-generated template # update description! +tags: # add any tags that will help you to track your manifests +initialize: + outputs: + - yaml # you can add - csv to export to csv + plugins: # add more plugins for your use-case + memory-energy-from-memory-util: # you can name this any way you like! + method: Coefficient # the name of the function exported from the plugin + path: "builtin" # the import path + global-config: # anmy config required by the plugin + input-parameter: "memory/utilization" + coefficient: 0.0001 #kwH/GB + output-parameter: "memory/energy" +tree: + children: # add a chile for each distinct component you want to measure + child: + pipeline: # the pipeline is an ordered list of plugins you want to execute + - memory-energy-from-memory-util # must match the name in initialize! + config: # any plugin specific, node-level config + inputs: + - timestamp: 2023-12-12T00:00:00.000Z # ISO 8061 string + duration: 3600 # units of seconds + memory/utilization: 10 diff --git a/src/config/params.ts b/src/config/params.ts index 377538391..76732e57b 100644 --- a/src/config/params.ts +++ b/src/config/params.ts @@ -62,12 +62,6 @@ export const PARAMETERS: Parameters = { unit: 'none', aggregation: 'sum', }, - 'functional-unit-time': { - description: - 'string describing the unit of time in which the final SCI calculation should be expressed, e.g. "1-min"', - unit: 'none', - aggregation: 'none', - }, 'gpu-util': { description: 'refers to CPU utilization.', unit: 'percentage', diff --git a/src/config/strings.ts b/src/config/strings.ts index a03192345..6a8a586f3 100644 --- a/src/config/strings.ts +++ b/src/config/strings.ts @@ -1,7 +1,6 @@ import {ManifestParameter} from '../types/manifest'; export const STRINGS = { - FILE_IS_NOT_YAML: 'Provided manifest is not in yaml format.', MANIFEST_IS_MISSING: 'Manifest is missing.', MISSING_METHOD: "Initalization param 'method' is missing.", MISSING_PATH: "Initalization param 'path' is missing.", @@ -17,12 +16,9 @@ Incubation projects are experimental, offer no support guarantee, have minimal g NOT_NATIVE_PLUGIN: (path: string) => ` You are using plugin ${path} which is not part of the Impact Framework standard library. You should do your own research to ensure the plugins are up to date and accurate. They may not be actively maintained.`, - SOMETHING_WRONG: 'Something wrong with cli arguments. Please check docs.', - ISSUE_TEMPLATE: ` -Impact Framework is an alpha release from the Green Software Foundation and is released to capture early feedback. If you'd like to offer some feedback, please use this issue template: -https://github.com/Green-Software-Foundation/if/issues/new?assignees=&labels=feedback&projects=&template=feedback.md&title=Feedback+-+`, - INVALID_MODULE_PATH: (path: string) => - `Provided module: '${path}' is invalid or not found.`, + INVALID_MODULE_PATH: (path: string, error?: any) => + `Provided module \`${path}\` is invalid or not found. ${error ?? ''} +`, INVALID_TIME_NORMALIZATION: 'Start time or end time is missing.', UNEXPECTED_TIME_CONFIG: 'Unexpected node-level config provided for time-sync plugin.', @@ -33,7 +29,6 @@ https://github.com/Green-Software-Foundation/if/issues/new?assignees=&labels=fee `Avoiding padding at ${ start && end ? 'start and end' : start ? 'start' : 'end' }`, - INVALID_OBSERVATION_OVERLAP: 'Observation timestamps overlap.', INVALID_AGGREGATION_METHOD: (metric: string) => `Aggregation is not possible for given ${metric} since method is 'none'.`, METRIC_MISSING: (metric: string, index: number) => @@ -53,9 +48,68 @@ You have not selected an output method. To see your output data, you can choose --output : this will save your output data to the given filepath (do not provide file extension) Note that for the '--output' option you also need to define the output type in your manifest file. See https://if.greensoftware.foundation/major-concepts/manifest-file#initialize`, SOURCE_IS_NOT_YAML: 'Given source file is not in yaml format.', - TARGET_IS_NOT_YAML: 'Given target is not in yaml format.', + TARGET_IS_NOT_YAML: 'Given target file is not in yaml format.', INVALID_TARGET: 'Target is invalid.', INVALID_SOURCE: 'Source is invalid.', + UNSUPPORTED_ERROR: (errorName: string) => + `UnsupportedErrorClass: plugin threw error class: ${errorName} that is not recognized by Impact Framework`, + /** Plugin messages */ + MISSING_GLOBAL_CONFIG: 'Global config is not provided.', + MISSING_INPUT_DATA: (param: string) => + `${param} is missing from the input array, or has nullish value.`, + MANIFEST_NOT_FOUND: 'Manifest file not found.', + INITIALIZING_PACKAGE_JSON: 'Initializing package.json.', + INSTALLING_NPM_PACKAGES: 'Installing npm packages...', + NOT_NUMERIC_VALUE: (str: any) => `${str} is not numberic.`, + MISSING_FUNCTIONAL_UNIT_CONFIG: + '`functional-unit` should be provided in your global config', + MISSING_FUNCTIONAL_UNIT_INPUT: + '`functional-unit` value is missing from input data or it is not a positive integer', + REGEX_MISMATCH: (input: any, match: string) => + `\`${input}\` does not match the ${match} regex expression`, + SCI_EMBODIED_ERROR: (unit: string) => + `invalid number. please provide it as \`${unit}\` to input`, + MISSING_MIN_MAX: 'Config is missing min or max value', + INVALID_MIN_MAX: (name: string) => + `Min value should not be greater than or equal to max value of ${name}`, + FILE_FETCH_FAILED: ( + filepath: string, + message: string + ) => `Failed fetching the file: ${filepath}. +${message}`, + FILE_READ_FAILED: ( + filepath: string, + error: string + ) => `Failed reading the file: ${filepath}. +${error}`, + MISSING_CSV_COLUMN: (columnName: string) => + `There is no column with the name: ${columnName}.`, + NO_QUERY_DATA: + 'One or more of the given query parameters are not found in the target CSV file column headers.', + INVALID_DATE_TYPE: (date: any) => + `Unexpected date datatype: ${typeof date}: ${date}`, + INVALID_OBSERVATION_OVERLAP: + 'Observation timestamps overlap, please check inputs.', + SCI_MISSING_FN_UNIT: (functionalUnit: string) => + `'carbon' and ${functionalUnit} should be present in your input data.`, + /** Exhaust messages */ + OUTPUT_REQUIRED: + 'Output path is required, please make sure output is configured properly.', + CSV_EXPORT: + 'CSV export criteria is not found in output path. Please append it after --output #.', + WRITE_CSV_ERROR: (outputPath: string, error: any) => + `Failed to write CSV file to ${outputPath}: ${error}`, + INVALID_NAME: + '`name` global config parameter is empty or contains all spaces', + START_LOWER_END: '`start-time` should be lower than `end-time`', + TIMESTAMP_REQUIRED: (index: number) => `required in input[${index}]`, + INVALID_DATETIME: (index: number) => `invalid datetime in input[${index}]`, + X_Y_EQUAL: 'The length of `x` and `y` should be equal', + ARRAY_LENGTH_NON_EMPTY: + 'the length of the input arrays must be greater than 1', + WITHIN_THE_RANGE: + 'The target x value must be within the range of the given x values', + /** Debugging logs */ STARTING_IF: 'Starting IF', EXITING_IF: 'Exiting IF', LOADING_MANIFEST: 'Loading manifest', @@ -80,4 +134,16 @@ Note that for the '--output' option you also need to define the output type in y `Exporting to csv file: ${savepath}`, EXPORTING_RAW_CSV_FILE: (savepath: string) => `Exporting raw csv file: ${savepath}`, + CHECKING: 'Checking...', + IF_CHECK_FLAGS_MISSING: + 'Either the `--manifest` or `--directory` command should be provided with a path', + DIRECTORY_NOT_FOUND: 'Directory not found.', + DIRECTORY_YAML_FILES_NOT_FOUND: + 'The directory does not contain any YAML/YML files.\n', + IF_CHECK_FAILED: (filename: string) => + `if-check could not verify ${filename}. The re-executed file does not match the original.\n`, + IF_CHECK_VERIFIED: (filename: string) => + `if-check successfully verified ${filename}\n`, + ZERO_DIVISION: (moduleName: string, index: number) => + `-- SKIPPING -- DivisionByZero: you are attempting to divide by zero in ${moduleName} plugin : inputs[${index}]\n`, }; diff --git a/src/diff.ts b/src/diff.ts index 71a57ed17..aad02482c 100644 --- a/src/diff.ts +++ b/src/diff.ts @@ -7,10 +7,11 @@ import {parseIfDiffArgs} from './util/args'; import {formatNotMatchingLog, parseManifestFromStdin} from './util/helpers'; import {validateManifest} from './util/validations'; -import {CONFIG} from './config'; import {logger} from './util/logger'; import {debugLogger} from './util/debug-logger'; +import {CONFIG} from './config'; + const {IF_DIFF} = CONFIG; const {SUCCESS_MESSAGE, FAILURE_MESSAGE} = IF_DIFF; diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 000000000..48b0b76b9 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/* eslint-disable no-process-exit */ +import {parseIfEnvArgs} from './util/args'; +import {logger} from './util/logger'; + +import {CONFIG} from './config'; + +import {EnvironmentOptions} from './types/if-env'; +import { + addTemplateManifest, + getOptionsFromArgs, + initializeAndInstallLibs, +} from './util/helpers'; + +const {IF_ENV} = CONFIG; +const {SUCCESS_MESSAGE} = IF_ENV; + +const IfEnv = async () => { + const commandArgs = await parseIfEnvArgs(); + const options: EnvironmentOptions = { + folderPath: process.env.CURRENT_DIR || process.cwd(), + install: !!commandArgs.install, + dependencies: {}, + cwd: !!commandArgs.cwd, + }; + + if (commandArgs && commandArgs.manifest) { + const {folderPath, install, dependencies} = + await getOptionsFromArgs(commandArgs); + options.folderPath = commandArgs.cwd ? options.folderPath : folderPath; + options.install = !!install; + options.dependencies = {...dependencies}; + } + + await initializeAndInstallLibs(options); + + if (!commandArgs || !commandArgs.manifest) { + await addTemplateManifest(options.folderPath); + } + + console.log(SUCCESS_MESSAGE); + process.exit(0); +}; + +IfEnv().catch(error => { + if (error instanceof Error) { + logger.error(error); + process.exit(2); + } +}); diff --git a/src/lib/aggregate.ts b/src/lib/aggregate.ts index 977f30300..c6eef11cc 100644 --- a/src/lib/aggregate.ts +++ b/src/lib/aggregate.ts @@ -1,10 +1,11 @@ -import {aggregateInputsIntoOne} from '../util/aggregation-helper'; +import {PluginParams} from '@grnsft/if-core/types'; -import {PluginParams} from '../types/interface'; -import {AggregationParams, AggregationParamsSure} from '../types/manifest'; +import {aggregateInputsIntoOne} from '../util/aggregation-helper'; import {STRINGS} from '../config/strings'; +import {AggregationParams, AggregationParamsSure} from '../types/manifest'; + const {AGGREGATING_NODE, AGGREGATING_OUTPUTS} = STRINGS; /** diff --git a/src/lib/compute.ts b/src/lib/compute.ts index 40db1a42a..b83a22d2e 100644 --- a/src/lib/compute.ts +++ b/src/lib/compute.ts @@ -1,9 +1,10 @@ +import {PluginParams, GroupByConfig} from '@grnsft/if-core/types'; + import {debugLogger} from '../util/debug-logger'; import {mergeObjects} from '../util/helpers'; import {ComputeParams, Node, Params} from '../types/compute'; -import {PluginParams, isExecute, isGroupBy} from '../types/interface'; -import {GroupByConfig} from '../types/group-by'; +import {isExecute, isGroupBy} from '../types/interface'; import {STRINGS} from '../config/strings'; diff --git a/src/lib/exhaust.ts b/src/lib/exhaust.ts index 6bcc9824a..1b65ca898 100644 --- a/src/lib/exhaust.ts +++ b/src/lib/exhaust.ts @@ -1,3 +1,5 @@ +import {ERRORS} from '@grnsft/if-core/utils'; + /** * @todo This is temporary solution, will be refactored to support dynamic plugins. */ @@ -6,15 +8,13 @@ import {ExportCSVRaw} from '../builtins/export-csv-raw'; import {ExportLog} from '../builtins/export-log'; import {ExportYaml} from '../builtins/export-yaml'; -import {ERRORS} from '../util/errors'; - import {STRINGS} from '../config'; import {ExhaustPluginInterface} from '../types/exhaust-plugin-interface'; import {Context} from '../types/manifest'; import {Options} from '../types/process-args'; -const {ExhaustError} = ERRORS; +const {InvalidExhaustPluginError} = ERRORS; const {INVALID_EXHAUST_PLUGIN, PREPARING_OUTPUT_DATA} = STRINGS; /** @@ -35,7 +35,7 @@ const initializeExhaustPlugin = (name: string): ExhaustPluginInterface => { case 'csv-raw': return ExportCSVRaw(); default: - throw new ExhaustError(INVALID_EXHAUST_PLUGIN(name)); + throw new InvalidExhaustPluginError(INVALID_EXHAUST_PLUGIN(name)); } }; diff --git a/src/lib/initialize.ts b/src/lib/initialize.ts index c66e08c28..212d98e37 100644 --- a/src/lib/initialize.ts +++ b/src/lib/initialize.ts @@ -1,6 +1,7 @@ -import pathLib = require('path'); +import * as path from 'node:path'; + +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../util/errors'; import {logger} from '../util/logger'; import {memoizedLog} from '../util/log-memoize'; import {pluginStorage} from '../util/plugin-storage'; @@ -11,7 +12,11 @@ import {PluginInterface} from '../types/interface'; import {GlobalPlugins, PluginOptions} from '../types/manifest'; import {PluginStorageInterface} from '../types/plugin-storage'; -const {ModuleInitializationError, PluginCredentialError} = ERRORS; +const { + PluginInitializationError, + MissingPluginMethodError, + MissingPluginPathError, +} = ERRORS; const {GITHUB_PATH, NATIVE_PLUGIN} = CONFIG; const { @@ -28,14 +33,11 @@ const { * Imports module by given `path`. */ const importModuleFrom = async (path: string) => { - try { - const module = await import(path); + const module = await import(path).catch(error => { + throw new PluginInitializationError(INVALID_MODULE_PATH(path, error)); + }); - return module; - } catch (error) { - logger.error(error); - throw new ModuleInitializationError(INVALID_MODULE_PATH(path)); - } + return module; }; /** @@ -52,23 +54,23 @@ const importAndVerifyModule = async (method: string, path: string) => { * Then checks if `path` is starting with github, then grabs the repository name. * Imports module, then checks if it's a valid plugin. */ -const handModule = (method: string, path: string) => { - console.debug(LOADING_PLUGIN_FROM_PATH(method, path)); +const handModule = (method: string, pluginPath: string) => { + console.debug(LOADING_PLUGIN_FROM_PATH(method, pluginPath)); - if (path === 'builtin') { - path = pathLib.normalize(`${__dirname}/../builtins`); + if (pluginPath === 'builtin') { + pluginPath = path.normalize(`${__dirname}/../builtins`); } else { - if (path?.startsWith(GITHUB_PATH)) { - const parts = path.split('/'); - path = parts[parts.length - 1]; + if (pluginPath?.startsWith(GITHUB_PATH)) { + const parts = pluginPath.split('/'); + pluginPath = parts[parts.length - 1]; } - if (!path.includes(NATIVE_PLUGIN)) { - memoizedLog(logger.warn, NOT_NATIVE_PLUGIN(path)); + if (!pluginPath.includes(NATIVE_PLUGIN)) { + memoizedLog(logger.warn, NOT_NATIVE_PLUGIN(pluginPath)); } } - return importAndVerifyModule(method, path); + return importAndVerifyModule(method, pluginPath); }; /** @@ -82,11 +84,11 @@ const initPlugin = async ( console.debug(INITIALIZING_PLUGIN(method)); if (!method) { - throw new PluginCredentialError(MISSING_METHOD); + throw new MissingPluginMethodError(MISSING_METHOD); } if (!path) { - throw new PluginCredentialError(MISSING_PATH); + throw new MissingPluginPathError(MISSING_PATH); } const plugin = await handModule(method, path); diff --git a/src/lib/load.ts b/src/lib/load.ts index 2ed335b6f..61f2b6cf5 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,6 +1,6 @@ import * as YAML from 'js-yaml'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from '../util/errors'; import {openYamlFileAsObject} from '../util/yaml'; import {readAndParseJson} from '../util/json'; @@ -11,7 +11,7 @@ import {Parameters} from '../types/parameters'; import {LoadDiffParams} from '../types/util/args'; import {Manifest} from '../types/manifest'; -const {CliInputError} = ERRORS; +const {CliSourceFileError} = ERRORS; const {INVALID_SOURCE, LOADING_MANIFEST} = STRINGS; @@ -43,7 +43,7 @@ export const loadIfDiffFiles = async (params: LoadDiffParams) => { const {sourcePath, targetPath, pipedSourceManifest} = params; if (!sourcePath && !pipedSourceManifest) { - throw new CliInputError(INVALID_SOURCE); + throw new CliSourceFileError(INVALID_SOURCE); } const loadFromSource = diff --git a/src/types/common.ts b/src/types/common.ts deleted file mode 100644 index f7d337392..000000000 --- a/src/types/common.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type KeyValuePair = { - [key: string]: any; -}; - -export type ConfigParams = Record; diff --git a/src/types/compute.ts b/src/types/compute.ts index 739d86742..2de3e6e33 100644 --- a/src/types/compute.ts +++ b/src/types/compute.ts @@ -1,4 +1,5 @@ -import {PluginParams} from './interface'; +import {PluginParams} from '@grnsft/if-core/types'; + import {Context} from './manifest'; import {PluginStorageInterface} from './plugin-storage'; diff --git a/src/types/group-by.ts b/src/types/group-by.ts deleted file mode 100644 index 72dd6d61b..000000000 --- a/src/types/group-by.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type GroupByConfig = { - group: string[]; -}; diff --git a/src/types/helpers.ts b/src/types/helpers.ts deleted file mode 100644 index ed413174b..000000000 --- a/src/types/helpers.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type ErrorFormatParams = { - scope?: string; - message: string; -}; diff --git a/src/types/if-env.ts b/src/types/if-env.ts new file mode 100644 index 000000000..03ace8ec9 --- /dev/null +++ b/src/types/if-env.ts @@ -0,0 +1,6 @@ +export type EnvironmentOptions = { + folderPath: string; + install: boolean; + cwd: boolean; + dependencies: {[path: string]: string}; +}; diff --git a/src/types/interface.ts b/src/types/interface.ts index f06449bc6..6834a3ebb 100644 --- a/src/types/interface.ts +++ b/src/types/interface.ts @@ -1,27 +1,4 @@ -import {GroupByConfig} from './group-by'; - -export type PluginParams = Record; - -export type ConfigParams = Record; - -export type ExecutePlugin = { - execute: ( - inputs: PluginParams[], - config?: Record - ) => PluginParams[] | Promise; - metadata: { - kind: string; - }; - [key: string]: any; -}; - -export type GroupByPlugin = { - execute: (inputs: PluginParams[], config: GroupByConfig) => {children: any}; - metadata: { - kind: string; - }; - [key: string]: any; -}; +import {ExecutePlugin, GroupByPlugin} from '@grnsft/if-core/types'; export type PluginInterface = ExecutePlugin | GroupByPlugin; diff --git a/src/types/npm.ts b/src/types/npm.ts new file mode 100644 index 000000000..cbb3e0680 --- /dev/null +++ b/src/types/npm.ts @@ -0,0 +1,3 @@ +export type PathWithVersion = {[path: string]: string}; + +export type ManifestPlugin = {[key: string]: {path: string; method: string}}; diff --git a/src/types/process-args.ts b/src/types/process-args.ts index 1a637082f..0f3799f5a 100644 --- a/src/types/process-args.ts +++ b/src/types/process-args.ts @@ -11,6 +11,17 @@ export interface IFDiffArgs { target: string; } +export interface IFEnvArgs { + manifest?: string; + install?: boolean; + cwd?: boolean; +} + +export interface IFCheckArgs { + manifest?: string; + directory?: string; +} + export interface Options { outputPath?: string; stdout?: boolean; diff --git a/src/util/aggregation-helper.ts b/src/util/aggregation-helper.ts index 3be503ffc..408a4e9f7 100644 --- a/src/util/aggregation-helper.ts +++ b/src/util/aggregation-helper.ts @@ -1,12 +1,13 @@ -import {ERRORS} from '../util/errors'; +import {ERRORS} from '@grnsft/if-core/utils'; +import {PluginParams} from '@grnsft/if-core/types'; + import {parameterize} from '../lib/parameterize'; import {CONFIG, STRINGS} from '../config'; import {AggregationResult} from '../types/aggregation'; -import {PluginParams} from '../types/interface'; -const {InvalidAggregationParamsError} = ERRORS; +const {InvalidAggregationMethodError, MissingAggregationParamError} = ERRORS; const {INVALID_AGGREGATION_METHOD, METRIC_MISSING} = STRINGS; const {AGGREGATION_ADDITIONAL_PARAMS} = CONFIG; @@ -19,7 +20,7 @@ const checkIfMetricsAreValid = (metrics: string[]) => { const method = parameterize.getAggregationMethod(metric); if (method === 'none') { - throw new InvalidAggregationParamsError( + throw new InvalidAggregationMethodError( INVALID_AGGREGATION_METHOD(metric) ); } @@ -41,7 +42,7 @@ export const aggregateInputsIntoOne = ( return inputs.reduce((acc, input, index) => { for (const metric of extendedMetrics) { if (!(metric in input)) { - throw new InvalidAggregationParamsError(METRIC_MISSING(metric, index)); + throw new MissingAggregationParamError(METRIC_MISSING(metric, index)); } /** Checks if metric is timestamp or duration, then adds to aggregated value. */ diff --git a/src/util/args.ts b/src/util/args.ts index dfbd7e703..31da90461 100644 --- a/src/util/args.ts +++ b/src/util/args.ts @@ -1,26 +1,44 @@ import * as path from 'path'; + import {parse} from 'ts-command-line-args'; +import {ERRORS} from '@grnsft/if-core/utils'; import {checkIfFileIsYaml} from './yaml'; -import {ERRORS} from './errors'; + +import {isDirectoryExists, isFileExists} from './fs'; + import {logger} from './logger'; import {CONFIG, STRINGS} from '../config'; -import {IFDiffArgs, IEArgs, ProcessArgsOutputs} from '../types/process-args'; +import { + IFDiffArgs, + IEArgs, + ProcessArgsOutputs, + IFEnvArgs, + IFCheckArgs, +} from '../types/process-args'; import {LoadDiffParams} from '../types/util/args'; -const {CliInputError} = ERRORS; +const { + ParseCliParamsError, + CliTargetFileError, + CliSourceFileError, + InvalidDirectoryError, + MissingCliFlagsError, +} = ERRORS; -const {IE, IF_DIFF} = CONFIG; +const {IE, IF_DIFF, IF_ENV, IF_CHECK} = CONFIG; const { - FILE_IS_NOT_YAML, MANIFEST_IS_MISSING, + MANIFEST_NOT_FOUND, NO_OUTPUT, SOURCE_IS_NOT_YAML, TARGET_IS_NOT_YAML, INVALID_TARGET, + IF_CHECK_FLAGS_MISSING, + DIRECTORY_NOT_FOUND, } = STRINGS; /** @@ -31,7 +49,7 @@ const validateAndParseProcessArgs = () => { return parse(IE.ARGS, IE.HELP); } catch (error) { if (error instanceof Error) { - throw new CliInputError(error.message); + throw new ParseCliParamsError(error.message); } throw error; @@ -57,7 +75,7 @@ const prependFullFilePath = (filePath: string) => { * 3. If output params are missing, warns user about it. * 3. Otherwise checks if `manifest` param is there, then processes with checking if it's a yaml file. * If it is, then returns object containing full path. - * 4. If params are missing or invalid, then rejects with `CliInputError`. + * 4. If params are missing or invalid, then rejects with `ParseCliParamsError`. */ export const parseIEProcessArgs = (): ProcessArgsOutputs => { const { @@ -85,10 +103,10 @@ export const parseIEProcessArgs = (): ProcessArgsOutputs => { }; } - throw new CliInputError(FILE_IS_NOT_YAML); + throw new CliSourceFileError(SOURCE_IS_NOT_YAML); } - throw new CliInputError(MANIFEST_IS_MISSING); + throw new CliSourceFileError(MANIFEST_IS_MISSING); }; /** -- IF Diff -- */ @@ -101,7 +119,7 @@ const validateAndParseIfDiffArgs = () => { return parse(IF_DIFF.ARGS, IF_DIFF.HELP); } catch (error) { if (error instanceof Error) { - throw new CliInputError(error.message); + throw new ParseCliParamsError(error.message); } throw error; @@ -116,7 +134,7 @@ export const parseIfDiffArgs = () => { if (target) { if (source && !checkIfFileIsYaml(source)) { - throw new CliInputError(SOURCE_IS_NOT_YAML); + throw new CliSourceFileError(SOURCE_IS_NOT_YAML); } if (checkIfFileIsYaml(target)) { @@ -131,8 +149,100 @@ export const parseIfDiffArgs = () => { return response; } - throw new CliInputError(TARGET_IS_NOT_YAML); + throw new CliTargetFileError(TARGET_IS_NOT_YAML); + } + + throw new ParseCliParamsError(INVALID_TARGET); +}; + +/** -- IF Env -- */ + +/** + * Parses `if-env` process arguments. + */ +const validateAndParseIfEnvArgs = () => { + try { + return parse(IF_ENV.ARGS, IF_ENV.HELP); + } catch (error) { + if (error instanceof Error) { + throw new ParseCliParamsError(error.message); + } + + throw error; + } +}; + +/** + * Checks if the `manifest` command is provided and it is valid manifest file. + */ +export const parseIfEnvArgs = async () => { + const {manifest, install, cwd} = validateAndParseIfEnvArgs(); + + if (manifest) { + const response = prependFullFilePath(manifest); + const isManifestFileExists = await isFileExists(response); + + if (!isManifestFileExists) { + throw new ParseCliParamsError(MANIFEST_NOT_FOUND); + } + + if (checkIfFileIsYaml(manifest)) { + return {manifest: response, install, cwd}; + } + + throw new CliSourceFileError(SOURCE_IS_NOT_YAML); + } + + return {install, cwd}; +}; + +/** -- IF Check -- */ + +/** + * Parses `if-check` process arguments. + */ +const validateAndParseIfCheckArgs = () => { + try { + return parse(IF_CHECK.ARGS, IF_CHECK.HELP); + } catch (error) { + if (error instanceof Error) { + throw new ParseCliParamsError(error.message); + } + + throw error; + } +}; + +/** + * Checks if either `manifest` or `directory` command is provided. + */ +export const parseIfCheckArgs = async () => { + const {manifest, directory} = validateAndParseIfCheckArgs(); + + if (manifest) { + const response = prependFullFilePath(manifest); + const isManifestFileExists = await isFileExists(response); + + if (!isManifestFileExists) { + throw new ParseCliParamsError(MANIFEST_NOT_FOUND); + } + + if (checkIfFileIsYaml(manifest)) { + return {manifest}; + } + + throw new CliSourceFileError(SOURCE_IS_NOT_YAML); + } else if (directory) { + const isDirExists = await isDirectoryExists(directory); + + if (!isDirExists) { + throw new InvalidDirectoryError(DIRECTORY_NOT_FOUND); + } + + const response = prependFullFilePath(directory); + + return {directory: response}; } - throw new CliInputError(INVALID_TARGET); + throw new MissingCliFlagsError(IF_CHECK_FLAGS_MISSING); }; diff --git a/src/util/errors.ts b/src/util/errors.ts deleted file mode 100644 index a65a603be..000000000 --- a/src/util/errors.ts +++ /dev/null @@ -1,35 +0,0 @@ -const CUSTOM_ERRORS = [ - 'CliInputError', - 'ConfigNotFoundError', - 'ConfigValidationError', - 'ExhaustError', - 'FileNotFoundError', - 'MakeDirectoryError', - 'ManifestValidationError', - 'ModuleInitializationError', - 'InputValidationError', - 'InvalidAggregationParamsError', - 'InvalidGroupingError', - 'PluginCredentialError', - 'PluginInitalizationError', - 'WriteFileError', - 'ConfigNotFoundError', -] as const; - -type CustomErrors = { - [K in (typeof CUSTOM_ERRORS)[number]]: ErrorConstructor; -}; - -export const ERRORS = CUSTOM_ERRORS.reduce((acc, className) => { - acc = { - ...acc, - [className]: class extends Error { - constructor(message: string) { - super(message); - this.name = this.constructor.name; - } - }, - }; - - return acc; -}, {} as CustomErrors); diff --git a/src/util/fs.ts b/src/util/fs.ts new file mode 100644 index 000000000..bf10af9ba --- /dev/null +++ b/src/util/fs.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; + +/** + * Checks if file exists with the given `filePath`. + */ +export const isFileExists = async (filePath: string) => { + try { + await fs.stat(filePath); + return true; + } catch (error) { + return false; + } +}; + +/** + * Checks if the directory exists with the given `filePath`. + */ +export const isDirectoryExists = async (directoryPath: string) => { + try { + await fs.access(directoryPath); + return true; + } catch (error) { + return false; + } +}; + +/** + * Gets all files that have either .yml or .yaml extension in the given directory. + */ +export const getYamlFiles = async (directory: string) => { + let yamlFiles: string[] = []; + + const files = await fs.readdir(directory); + + for (const file of files) { + const fullPath = path.join(directory, file); + const stat = await fs.lstat(fullPath); + + if (stat.isDirectory()) { + yamlFiles = yamlFiles.concat(await getYamlFiles(fullPath)); + } else { + if (file.endsWith('.yml') || file.endsWith('.yaml')) { + yamlFiles.push(fullPath); + } + } + } + + return yamlFiles; +}; + +/** + * Gets fileName from the given path without an extension. + */ +export const getFileName = (filePath: string) => { + const baseName = path.basename(filePath); + const extension = path.extname(filePath); + return baseName.replace(extension, ''); +}; + +/** + * Removes the given file if exists. + */ +export const removeFileIfExists = async (filePath: string) => { + if (await isFileExists(filePath)) { + await fs.unlink(filePath); + } +}; diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 2b2dba87f..0fcdda963 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -1,24 +1,40 @@ +#!/usr/bin/env node +/* eslint-disable no-process-exit */ import {createInterface} from 'node:readline/promises'; import {exec} from 'node:child_process'; import {promisify} from 'node:util'; -import {ErrorFormatParams} from '../types/helpers'; -import {ERRORS} from './errors'; -import {logger} from './logger'; -import {STRINGS} from '../config'; + +import * as fs from 'fs/promises'; +import * as path from 'path'; + +import {ERRORS} from '@grnsft/if-core/utils'; + +import {STRINGS, CONFIG} from '../config'; import {Difference} from '../types/lib/compare'; -const {ISSUE_TEMPLATE} = STRINGS; +import {load} from '../lib/load'; -/** - * Formats given error according to class instance, scope and message. - */ -export const buildErrorMessage = - (classInstanceName: string) => (params: ErrorFormatParams) => { - const {scope, message} = params; +import { + installDependencies, + initPackageJsonIfNotExists, + updatePackageJsonDependencies, + extractPathsWithVersion, + updatePackageJsonProperties, +} from './npm'; - return `${classInstanceName}${scope ? `(${scope})` : ''}: ${message}.`; - }; +import {logger} from './logger'; +import {EnvironmentOptions} from '../types/if-env'; + +const {IF_ENV} = CONFIG; +const { + FAILURE_MESSAGE, + FAILURE_MESSAGE_TEMPLATE, + FAILURE_MESSAGE_DEPENDENCIES, +} = IF_ENV; + +const {UNSUPPORTED_ERROR, IF_CHECK_FAILED} = STRINGS; +const {MissingPluginDependenciesError} = ERRORS; /** * Impact engine error handler. Logs errors and appends issue template if error is unknown. @@ -29,7 +45,9 @@ export const andHandle = (error: Error) => { logger.error(error); if (!knownErrors.includes(error.name)) { - logger.warn(ISSUE_TEMPLATE); + logger.error(UNSUPPORTED_ERROR(error.name)); + // eslint-disable-next-line no-process-exit + process.exit(2); } }; @@ -182,3 +200,84 @@ export const parseManifestFromStdin = async () => { return match![1]; }; + +/** + * Gets the folder path of the manifest file, dependencies from manifest file and install argument from the given arguments. + */ +export const getOptionsFromArgs = async (commandArgs: { + manifest: string; + install: boolean | undefined; +}) => { + const {manifest, install} = commandArgs; + const folderPath = path.dirname(manifest); + const loadedManifest = await load(manifest); + const rawManifest = loadedManifest.rawManifest; + const plugins = rawManifest?.initialize?.plugins || {}; + const dependencies = rawManifest?.execution?.environment.dependencies || []; + + if (!dependencies.length) { + throw new MissingPluginDependenciesError(FAILURE_MESSAGE_DEPENDENCIES); + } + + const pathsWithVersion = extractPathsWithVersion(plugins, dependencies); + + return { + folderPath, + dependencies: pathsWithVersion, + install, + }; +}; + +/** + * Creates folder if not exists, installs dependencies if required, update depenedencies. + */ +export const initializeAndInstallLibs = async (options: EnvironmentOptions) => { + try { + const {folderPath, install, cwd, dependencies} = options; + const packageJsonPath = await initPackageJsonIfNotExists(folderPath); + + await updatePackageJsonProperties(packageJsonPath, cwd); + + if (install) { + await installDependencies(folderPath, dependencies); + } else { + await updatePackageJsonDependencies(packageJsonPath, dependencies, cwd); + } + } catch (error) { + console.log(FAILURE_MESSAGE); + process.exit(2); + } +}; + +/** + * Adds a manifest template to the folder where the if-env CLI command runs. + */ +export const addTemplateManifest = async (destinationDir: string) => { + try { + const templateManifest = path.resolve( + __dirname, + '../config/env-template.yml' + ); + + const destinationPath = path.resolve(destinationDir, 'manifest.yml'); + const data = await fs.readFile(templateManifest, 'utf-8'); + + await fs.writeFile(destinationPath, data, 'utf-8'); + } catch (error) { + console.log(FAILURE_MESSAGE_TEMPLATE); + process.exit(1); + } +}; + +/** + * Logs the failure message from the stdout of an error. + */ +export const logStdoutFailMessage = (error: any, fileName: string) => { + console.log(IF_CHECK_FAILED(fileName)); + + const stdout = error.stdout; + const logs = stdout.split('\n\n'); + const failMessage = logs[logs.length - 1]; + + console.log(failMessage); +}; diff --git a/src/util/npm.ts b/src/util/npm.ts new file mode 100644 index 000000000..de27ae4de --- /dev/null +++ b/src/util/npm.ts @@ -0,0 +1,193 @@ +#!/usr/bin/env node +/* eslint-disable no-process-exit */ +import * as fs from 'fs/promises'; +import * as path from 'path'; + +import {execPromise} from './helpers'; +import { + isDirectoryExists, + getFileName, + isFileExists, + removeFileIfExists, +} from './fs'; +import {logger} from './logger'; + +import {STRINGS} from '../config'; +import {ManifestPlugin, PathWithVersion} from '../types/npm'; + +const packageJson = require('../../package.json'); + +const {INITIALIZING_PACKAGE_JSON, INSTALLING_NPM_PACKAGES, IF_CHECK_VERIFIED} = + STRINGS; + +/** + * Checks if the package.json is exists, if not, initializes it. + */ +export const initPackageJsonIfNotExists = async (folderPath: string) => { + const packageJsonPath = path.resolve(folderPath, 'package.json'); + const isPackageJsonExists = await isFileExists(packageJsonPath); + + if (!isPackageJsonExists) { + logger.info(INITIALIZING_PACKAGE_JSON); + + const nodeModulesPath = path.resolve(folderPath, 'node_modules'); + const isNodeModulesExists = await isDirectoryExists(nodeModulesPath); + + if (isNodeModulesExists) { + await fs.rm(nodeModulesPath, {recursive: true}); + } + + await execPromise('npm init -y', {cwd: folderPath}); + } + + return packageJsonPath; +}; + +/** + * Installs packages from the specified dependencies in the specified folder. + */ +export const installDependencies = async ( + folderPath: string, + dependencies: {[path: string]: string} +) => { + const packages = Object.entries(dependencies).map( + ([dependency, version]) => `${dependency}@${version.replace('^', '')}` + ); + + logger.info(INSTALLING_NPM_PACKAGES); + await execPromise(`npm install ${packages.join(' ')}`, { + cwd: folderPath, + }); +}; + +/** + * Updates package.json dependencies. + */ +export const updatePackageJsonDependencies = async ( + packageJsonPath: string, + dependencies: PathWithVersion, + cwd: boolean +) => { + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); + + const parsedPackageJson = JSON.parse(packageJsonContent); + + if (cwd) { + parsedPackageJson.dependencies = { + ...parsedPackageJson.dependencies, + ...dependencies, + }; + } else { + parsedPackageJson.dependencies = {...dependencies}; + } + + await fs.writeFile( + packageJsonPath, + JSON.stringify(parsedPackageJson, null, 2) + ); +}; + +/** + * Gets depencecies with versions. + */ +export const extractPathsWithVersion = ( + plugins: ManifestPlugin, + dependencies: string[] +) => { + const paths = Object.keys(plugins).map(plugin => plugins[plugin].path); + const uniquePaths = [...new Set(paths)].filter(path => path !== 'builtin'); + const pathsWithVersion: PathWithVersion = {}; + + uniquePaths.forEach(pluginPath => { + const dependency = dependencies.find((dependency: string) => + dependency.startsWith(pluginPath) + ); + + if (dependency) { + const splittedDependency = dependency.split('@'); + const version = + splittedDependency.length > 2 + ? splittedDependency[2].split(' ')[0] + : splittedDependency[1]; + + pathsWithVersion[pluginPath] = `^${version}`; + } + }); + + return pathsWithVersion; +}; + +/** + * Update the package.json properties. + */ +export const updatePackageJsonProperties = async ( + newPackageJsonPath: string, + appendDependencies: boolean +) => { + const packageJsonContent = await fs.readFile(newPackageJsonPath, 'utf8'); + const parsedPackageJsonContent = JSON.parse(packageJsonContent); + + const properties = { + name: 'if-environment', + description: packageJson.description, + author: packageJson.author, + bugs: packageJson.bugs, + engines: packageJson.engines, + homepage: packageJson.homepage, + dependencies: appendDependencies + ? parsedPackageJsonContent.dependencies + : {}, + }; + + const newPackageJson = Object.assign( + {}, + parsedPackageJsonContent, + properties + ); + + await fs.writeFile( + newPackageJsonPath, + JSON.stringify(newPackageJson, null, 2) + ); +}; + +/** + * Executes a series of npm commands based on the provided manifest file. + */ +export const executeCommands = async (manifest: string, cwd: boolean) => { + // TODO: After release remove isGlobal and appropriate checks + const isGlobal = !!process.env.npm_config_global; + const manifestDirPath = path.dirname(manifest); + const manifestFileName = getFileName(manifest); + const executedManifest = path.join(manifestDirPath, `re-${manifestFileName}`); + const prefixFlag = + process.env.CURRENT_DIR && process.env.CURRENT_DIR !== process.cwd() + ? `--prefix=${path.relative(process.env.CURRENT_DIR!, process.cwd())}` + : ''; + const ifEnv = `${ + isGlobal ? `if-env ${prefixFlag}` : `npm run if-env ${prefixFlag} --` + } -m ${manifest}`; + const ifEnvCommand = cwd ? `${ifEnv} -c` : ifEnv; + const ifRunCommand = `${ + isGlobal ? `if-run ${prefixFlag}` : `npm run if-run ${prefixFlag} --` + } -m ${manifest} -o ${executedManifest}`; + const ifDiffCommand = `${ + isGlobal ? `if-diff ${prefixFlag}` : `npm run if-diff ${prefixFlag} --` + } -s ${executedManifest}.yaml -t ${manifest}`; + const ttyCommand = " node -p 'Boolean(process.stdout.isTTY)'"; + + await execPromise( + `${ifEnvCommand} && ${ifRunCommand} && ${ttyCommand} | ${ifDiffCommand}`, + { + cwd: process.env.CURRENT_DIR || process.cwd(), + } + ); + + if (!cwd) { + await removeFileIfExists(`${manifestDirPath}/package.json`); + } + + await removeFileIfExists(`${executedManifest}.yaml`); + + console.log(IF_CHECK_VERIFIED(path.basename(manifest))); +}; diff --git a/src/util/plugin-storage.ts b/src/util/plugin-storage.ts index 7667d5e07..1f9711943 100644 --- a/src/util/plugin-storage.ts +++ b/src/util/plugin-storage.ts @@ -1,10 +1,11 @@ -import {ERRORS} from '../util/errors'; +import {ERRORS} from '@grnsft/if-core/utils'; + import {STRINGS} from '../config'; import {PluginInterface} from '../types/interface'; import {PluginStorage} from '../types/plugin-storage'; -const {PluginInitalizationError} = ERRORS; +const {PluginInitializationError} = ERRORS; const {NOT_INITALIZED_PLUGIN} = STRINGS; /** @@ -21,7 +22,7 @@ export const pluginStorage = () => { const plugin = storage[name]; if (!plugin) { - throw new PluginInitalizationError(NOT_INITALIZED_PLUGIN(name)); + throw new PluginInitializationError(NOT_INITALIZED_PLUGIN(name)); } return plugin; diff --git a/src/util/validations.ts b/src/util/validations.ts index 21695f3ee..920e7cbdb 100644 --- a/src/util/validations.ts +++ b/src/util/validations.ts @@ -1,12 +1,11 @@ import {ZodIssue, ZodIssueCode, ZodSchema, z} from 'zod'; +import {ERRORS} from '@grnsft/if-core/utils'; -import {ERRORS} from './errors'; +import {STRINGS} from '../config/strings'; import {AGGREGATION_METHODS} from '../types/aggregation'; import {AGGREGATION_TYPES} from '../types/parameters'; -import {STRINGS} from '../config/strings'; - const {ManifestValidationError, InputValidationError} = ERRORS; const {VALIDATING_MANIFEST} = STRINGS;