Currently as this is not distributed as a ready to go package you will need to
pull the repo to your Max 9 Packages folder, have pnpm installed and run
pnpm build to set up this repo to be ready to work.
mUnit is comprised of two main objects, and some helpers, which allow you to build up test suites for your max objects and abstractions.
One complication of note is that if you try to [loadbang] things in a test patch it may not work and the assertions will not be executed. This is simply due to the loadbang being too fast for the v8 and node.script objects that comprise the test framework. The node.script object has some workarounds for this, but it's not so easy to do in the assert objects.
The [mUnit.loadbang] object is provided to send a loadbang when the test framework is ready, this is especially useful in the test setups where you use the wrangler object and open multilpe patches.
For single use tests which also contain [mUnit.handler] this object has an
outlet which will send a bang when the node.script which power mUnit is done
loading, you could also use this to ensure your tests run at an appropriate
time. You can access this outside of patches which contain a handler by using
[r munit.loadbang]
The assert object is what you will send inputs into to be validated against an expression. Any expression which can be parsed by eval in v8 is supported. You should use inX in your expression code to describe how many inputs your object requires, and these will be substituted at runtime with the values you supply. eg
in1 > in2Test inputs should be patched to inlet 1+ in the object. The number of inputs to the object will be dynamically created depending on the number of inX values in your assertion expression, UNLESS you are passing a .js expression (see below).
You may have no more than 10 seperate inputs. Data passed to these inputs will be packed inside the assert object, therefore the first data inlet (the leftmost data inlet) is the hot input and will trigger the data to be evaluated.
If you do not supply an assertion the default assertion will be used
in1 === 1Additionally you can pass in the name of a javascript file which will be used for evaluation. It is important that this script exports an assertion function as the default export and that it returns an mUnitAssertion State value. For example if you wanted to implement a script which ensured that every input was unique you could do it as follows:
const mLib = require("mUnitLib.js");
const hist = [];
function assert(data) {
const found = hist.indexOf(data[0]);
if (found > -1) {
return mLib.State.Fail;
}
hist.push(data[0]);
return mLib.State.Continue;
}
module.exports = assert;Note that you don't need to return an explicit pass in this script, this is
managed by the @maxCalls of the object, it will natuarally succeed once
@maxCalls has passed without a failing result occuring. If you wanted your
assertion to explicitly pass on a specific value being received you can return
mAssert.State.Pass and the object will be marked as passed with no further
evaluations undertaken.
It is important to note that all the data for your assertion function will be provided as a single data array.
Unlike using a simple expression a js expression cannot be evaluated dynamically to create the number of inputs, therefore you should use the @dataInlets attr to set this. Setting the inlets attr when using a simple evaluation function will have no effect.
You should require "mUnitAssertion.js" in your assertion scripts to allow you to access the test return states, and for convenience functions such as well formed logging. See mUnitAssertions.js for what is made available.
There are additional attrs which can be passed to the object
@maxCalls (default 1) - the number of times the object can receive input before failing the test and rejecting further inputs. The assertion will not fail until it has received maxCalls incorrect inputs Once a passing state has been achieved no further calls are recorded
@name (default null) - A name for the test, used in logs and results. If not set will be set to a UUID. A test will always be given a UUID which will change on each run and will be seen in all logging. This allows you to tie data together across the logging and results output files.
@suite (default null) - A suite for the tests, if not set will use the parent patcher name
@accumulate (default false) - If true then accumulate the input data over the course of successive runs before evaluating the expression. Only works for simple exprs, to do this in js scripts you would need to implement it within your script (for now!)
@dataInlets (default 1) - If passing a file.js insead of an evaluation expression to the object you can use the dataInlets attr to specifically set the number of inlets for the object. If this attr is used when passing a simple evaluation expression it will be ignored
@timeout (default -1) - the amount of time in which the test should end. The
default -1 means there is no timeout and the test will not report its results
until a result state has been achieved, either by passing or by the maxCalls
being reached. Timeout is set in ms, so @timeout 1000 means 1 second. If this
is used together with @maxCalls then the timer will be rescheduled on each fail
until the maximum number of calls is reached eg @timeout 2000 @maxCalls 3 will
have a maximum of 6 seconds in which to pass.
Log messages from assert objects follow a strict formatting of
"munit" level suite uuid name ":" msgIn general log messages will appear in the output log in the format of
time : date - callsite : level(numerical) : message
which means that with the example of munit logs a message may look like
time: date - node.script : 0 : [ munit Registered Assertion customEverything 1108 defaultTest ]
## mUnit.handler
The Handler orchestrates a suite of tests, either in the current patch for small
test suites, or from a test harness patch which will be used to set up running
more complex test scenarios.
### Configuration
Simple configurations of the handler is passed as a dict object to the first
inlet prepended by the `configure` message. The configuration is as follows
```JSON
{
"logFormatter": null,
"resultFormatter": null,
"ignoreConsoleErrors": true,
"logOutputFilters": [],
"logObjectFilters": ["print"],
"outputDir": "/full/path/to/my/output/dir",
}
It is very important to note that using "./" as the output dir will not output the files where you expect, because the path is relative to the code that is calling it! Therefore you must supply the full path to the output directory here. You can use a tilde for the home directory as this will be resolved. YMMV doing this in windows though!
The prefered method is to create an munit.config.js file in the root folder
of your tests which describes your config
/**
* @see
* @type {import("munit-lib").HandlerConfig}
*/
const config = {
logFormatter: "DefaultLogFormatter",
resultFormatter: null,
ignoreConsoleErrors: true,
logOutputFilters: [],
logObjectFilters: ["print"],
outputDir: "./",
};
module.exports = config;Then when you create your test suite you should connect the [mUnit.startup]
object to the [mUnit.handler] object. With this configuration in place mUnit
will be able to find the local config file, and it will be able to rewrite
outputDir: "./" to the actual path that the tests are being called from.
The log formatter is used to render the output of the logs when the tests are finished. You can use this to conform the log messages to a specific format, for example if your cloud provider shows logs in a special way in the console.
The default implementation will simply render the logs to a text file in the same format it is written to the test framework.
To implement your own log formatter implement the following interface:
interface LogFormatterOptions { }
interface LogFormatter {
configure(config: LogFormatterOptions): void
render(log: string[]): string
fileExtension(): string
}If you make your own log formatter please consider making a PR to add it. If you
do so please also add it to the createIncludedLogFormatter function so it can
be looked up by a string name from the config.
The result formatter tells the handler how to render the results. The default format is for Json. Please do consider contributing any formatter you may create.
There is additionally a basic result formatted for JUnit, based on information about the format found here
If set to false then any error written to the console will mark the entire test suite as failed. This is set to false by default.
Log output filters will filter specific messages from the logs before they are rendered to disk by the log formatter. This is useful if you want to capture something internally, perhaps for interrogating the logs locally, but you don't want it to render to disk. This can also be used to ensure that secrets are not logged to the console of your cloud provider/test runner. Log output filters should be an array of Javascript regexp patterns !!!!!!!NOT CURRENTLY IMPLEMENTED!!!!!!
This will filter incoming messages from the max console before they are captured by the handlers log. node.script messages are filtered by default since these can cause recursions in the handler. Log input filters are specified as an array of strings specifying max Object Names. Note that these are filtered at the node.js level and are not arguments to the max console object.
This is a helper patch to make it easier to start up mUnit with a .js config
and a number of test files in a directory.
If you want to have a master test handler patch and open lots of individual test
files this is the approach to use. See the loadInFolderExample for how to do this.
This object will open all the maxpatches in a folder simultaneously and execute them. It will then close them all once the tests have all finished.
Source code for this library is written mostly in Typescript, and targets both the v8 and node.script objects in max. You will need to build both packages to get a fully working setup. Please ensure any contributions are formatted with prettier using the provided config.
If you are adding to mUnit.handler it's preferable to not bring in any node dependencies because it's not really fun or easy to make these distributable inside max and we are trying hard to avoid using a bundler with this project.
AGPL OR Proprietary
mUnit operates under a dual licence, it is offered as a permissive AGPL licence for open source projects. Those seeking to create and commercially distribute proprietary derivative works incorporating mUnit or those using mUnit in CI, testing or any other scenarios for their own commercial software, who do not wish to be subject to the restrictions and obligations of the GPL must contact the author to obtain a proprietary licence.
Because of the dual licencing model of this software, before any contributions to the software are made by third party authors, they must sign a Contributor License Agreement where you grant rights to the original author to relicense your contribution in both GPL and proprietary licenses. If you have any concerns about this please get in touch.