DependencyControl provides versioning, automatic script update, dependency management and script management services to Aegisub macros and modules.
Features:
- A lightweight package manager lets users conveniently install scripts right from inside Aegisub
- Loads modules used by an automation script, pulls missing requirements from the internet and informs the user about missing and outdated modules that could not be updated automatically
- Checks scripts and modules for updates and automatically installs them
- Offers convenient macro registration with user-customizable submenus
- Provides configuration, logging services, file operations and a unit test framework for your scripts
- Supports optional modules and private module copies for cases where an older or custom version of a module is required
- Resolves circular dependencies (limitations apply)
Requirements:
- Aegisub > 3.2.0 (e.g. Plorkyeran's r8792+ or my git builds)
- LuaJSON
- DownloadManager v0.3.0
- BadMutex v0.1.2
- PreciseTimer v0.1.4
- DependencyControl for Users
- Usage for Automation Scripts
- Namespaces and Paths
- The Anatomy of an Updater Feed
- Reference
- DependencyControl
- Updater
- Logger
- ConfigHandler
- FileOps
As an end-user you don't get to decide whether your scripts use DependencyControl or not, but you can control many aspects of its operation. The updater works out-of-the-box (for any script with an update feed) and is run automatically.
- Download the latest DependencyControl release for your platform and unpack its contents to your Aegisub user automation directory. Alternatively use one of the provided Aegisub builds with built-in DependencyControl.
It is essential DependencyControl and all scripts it's used reside in the user automation directory, NOT the the automation directory in the Aegisub application folder.
On Windows, this will be %AppData%\Aegisub\automation
folder.
- In Aegisub, rescan your automation folder (or restart Aegisub).
DependencyControl comes with sane default settings, so if you're happy with that, there's no need to read further. If you want to disable the updater, use custom menus or want to tweak another aspect of DepedencyControl, read on.
DependencyControl stores its configuration as a JSON file in the config subdirectory of your Aegisub folder (l0.DependencyControl.json
). Currently you'll have to edit this file manually, in the future there will be a management macro.
There are 2 kinds of configuration:
Changes made in the config
section of the configuration file will affect all scripts and general DependencyControl behavior.
Available Fields:
- bool updaterEnabled [true]: Turns the updater on/off
- int updateInterval [3 Days]: The time in seconds between two update checks of a script
- int traceLevel [3]: Sets the Trace level of DependencyControl update messages. Setting this higher than your Trace level setting in Aegisub will prevent any of the messages from littering your log window.
- bool dumpFeeds [true]: Debug option that will make DependencyControl dump updater feeds (original and expanded) to your Aegsiub folder.
- arr extraFeeds: lets you provide additional update feeds that will be used when checking any script for updates
- bool tryAllFeeds [false]: When set to true, feeds available to update a macro or module will be checked until an update is found. When set to false, a regular update process will stop once a feed confirms the script to be up-to-date.
- str configDir ["?user/config"]: Sets the configuration directory that will be "offered" to automation scripts (they may or may not actually use it)
- str writeLogs [true]: When enabled, DependencyControl log messages will be written to a file in the Aegisub log folder. This is a valuable resource for debugging, especially since the Aegisub log window is not available during script initalization.
- int logMaxFiles [200]: DepedencyControl will purge old updater log files when any of the limits for log file count, log age and cumulative file size is exceeded.
- int logMaxAge [1 Week]: Logs with a last modified date that exceeds this limit will be deleted. Takes a duration in seconds.
- int logMaxSize [10 MB]: Cumulative file size limit for all log files in bytes.
Changes made in the macros
and modules
sections of the configuration file affect only the script or module in question.
Available Fields:
- str customMenu: If you want to sort your automation macros into submenus, set this to the submenu name (use
/
to denote submenu levels). - str userFeed: When set the updater will use this feed exclusively to update the script in question (instead of other feeds)
- int lastUpdateCheck [auto]: This field is used to store the (epoch) time of the last update check.
- int logLevel [3]: sets the default trace level for log messages from this script (only applies to messages sent through a Logger instance provided by DepedencyControl to the script)
- bool logToFile [false]: set the user preference wrt/ whether log messages of this script should be written to disk or not (same restrictions as above apply, may be overridden by the script)
- author, configFile, feed, moduleName, name, namespace, url, requiredModules, version, unmanaged: These fields hold aspects of the script's version record. Don't change them (they will be reset anyway)
Load DependencyControl at the start of your macro and create a version record. Script and version information is automatically pulled from the script_*
variables (the additional script_namespace
variable is required).
Here's an example of a macro that requires several modules - some of which have a version record as well as some that don't.
script_name = "Move Along Path"
script_description = "Moves text along a path specified in a \\clip. Currently only works on fbf lines."
script_version = "0.1.2"
script_author = "line0"
script_namespace = "l0.MoveAlongPath"
local DependencyControl = require("l0.DependencyControl")
local version = DependencyControl{
feed = "https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/DependencyControl.json",
{
"aegisub.util",
{"a-mo.LineCollection", version="1.0.1", url="https://github.com/torque/Aegisub-Motion"},
{"a-mo.Line", version="1.0.0", url="https://github.com/TypesettingTools/Aegisub-Motion"},
{"a-mo.Log", url="https://github.com/torque/Aegisub-Motion"},
{"l0.ASSFoundation", version="0.1.1", url="https://github.com/TypesettingTools/ASSFoundation",
feed = "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"},
{"l0.ASSFoundation.Common", version="0.1.1", url="https://github.com/TypesettingTools/ASSFoundation",
feed = "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"},
"YUtils"
}
}
local util, LineCollection, Line, Log, ASS, Common, YUtils = version:requireModules()
Specifying a feed in your own version record provides DepedencyControl with a source to download updates to your script from. Specifying feeds for required modules managed by DependencyControl allows the Updater to discover those modules and fetch them when they're missing from the user's computer. However, you can omit the feed URLs for required modules when your own feed already has references to them.
To register your macros use the following code snippets instead of the usual aegisub.register_macro() calls:
For a single macro that should be registered using the script_name as automation menu entry, use:
version:registerMacro(myProcessingFunction)
For a script that registers several macros using its own submenu use:
version:registerMacros{
{script_name, "Opens the Move Along Path GUI", showDialog, validClip},
{"Undo", "Reverts lines to their original state", undo, hasUndoData}
}
Using this method for macro registration is a requirement for the custom submenus feature to work with your script and lets DependencyControl hook your macro processing function to run an update check when your macro is run.
Creating a record for a module is very similar to how it does for macros, with the key difference being that name and version information is passed to DependencyControl correctly and a moduleName is required.
local DependencyControl = require("l0.DependencyControl")
local version = DependencyControl{
name = "ASSFoundation",
version = "0.1.1",
description = "General purpose ASS processing library",
author = "line0",
url = "http://github.com/TypesettingTools/ASSFoundation",
moduleName = "l0.ASSFoundation",
feed = "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json",
{
"l0.ASSFoundation.ClassFactory",
"aegisub.re", "aegisub.util", "aegisub.unicode",
{"l0.ASSFoundation.Common", version="0.1.1", url="https://github.com/TypesettingTools/ASSFoundation",
feed = "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"},
{"a-mo.LineCollection", version="1.0.1", url="https://github.com/TypesettingTools/Aegisub-Motion"},
{"a-mo.Line", version="1.0.0", url="https://github.com/TypesettingTools/Aegisub-Motion"},
{"a-mo.Log", url="https://github.com/TypesettingTools/Aegisub-Motion"},
"ASSInspector.Inspector",
{"YUtils", optional=true},
}
local createASSClass, re, util, unicode, Common, LineCollection, Line, Log, ASSInspector, YUtils = version:requireModules()
A reference to the version record must be added as the .version field of your returned module for version control to work. A module should also register itself to enable circular dependency support. The :register() method returns your module, so the last lines of your module should look like this:
MyModule.version = version
return version:register(MyModule)
DependencyControl strictly enforces a namespace-based file structure for modules as well as automation macros in order to ensure there are no conflicts between scripts that happen to have the same name.
Automation scripts must define their namespace in the version record whereas for modules the module name (as you would use in a require
statement) defines the namespace.
- contains at least one dot
- must not start or end with a dot
- must not contain series of two or more dots
- the character set is restricted to:
A-Z
,a-z
,0-9
,.
,_
,-
- should be descriptive (this is more of a guideline)
Examples:
- l0.ASSFoundation
- l0.ASSFoundation.Common (for a separately version-controlled 'submodule')
- l0.ASSWipe
- a-mo.LineCollection
The namespace of your script translates into a subtree of the userautomation directory you can use to store your files in. DepedencyControl will not refuse to work with scripts that ignore this restriction, however it's designed in such a way that downloading to locations outside of your tree is impossible (which means your macro/module be able to use the auto-updater).
Automation Scripts use the ?user/automation/autoload
, which has a flat file structure. You may not use subdirectories and your file names must start with the namespace of your script.
Examples:
- l0.ASSWipe.lua
- l0.ASSWipe.Addon.moon
Modules use the ?user/automation/include
folder, which has a nested file structure. To determine your subdirectory/file base name, the dots in your namespace are replaced with /
(\
in Windows terms).
Tests use the ?user/automation/tests/DepUnit/modules
or ?user/automation/tests/DepUnit/macros
folder depending on whether a macro or automation is being tested and mirror the directory structure of the respective include
and autoload
folders.
Our example module ASSFoundation with namespace l0.ASSFoundation writes (among others) the following files:
- ?user/automation/include/l0/ASSFoundation.lua
- ?user/automation/include/l0/ASSFoundation/ClassFactory.lua
- ?user/automation/include/l0/ASSFoundation/Draw/Bezier.lua
- ?user/automation/tests/modules/l0/ASSFoundation.lua
If you want DepedencyControl auto-update your script on the user's system, you'll need to supply update information in an updater feed, which is a JSON file with a simple basic layout:
(//
denotes a comment explaining the property above)
{
"dependencyControlFeedFormatVersion": "0.3.0",
// The version of the feed format. The current version is 0.3.0, don't touch this until further notice.
"name": "line0's Aegisub Scripts",
"description": "Main repository for all of line0's automation macros.",
"maintainer": "line0",
// The title and description of your repository as well as the name of the maintainer. May be used by GUI-driven management tools, package managers, etc...
"knownFeeds": {
"a-mo": "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json",
"ASSFoundation": "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"
},
// A hashtable of known feed URLs. Can be referenced with @{feed:name} and will be used to discover other repositories the user can install automation scripts and modules from. At the very least this should contain the repo URLs for the required modules in your repo, but may be used to advertise other unrelated repos you trust.
"baseUrl": "https://github.com/TypesettingTools/line0-Aegisub-Scripts",
// baseUrl is a template variable that can be referenced in other string fields of the template. It's useful when you have several scripts which all have their documentation hosted on the same site (so they start with the same URL). For more Information about templates, see the section below.
"url": "@{baseUrl}",
// The address where information about this repository can be found. In this case it references the baseUrl template variable and expands to "https://github.com/TypesettingTools/line0-Aegisub-Scripts".
"fileBaseUrl": "https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/@{channel}/@{namespace}",
// A special rolling template variable. See the templates section below for more information.
"macros": {
// the section where all automation scripts tracked by this feed go. The key for each value is the namespace of the respective script. Below this level, this namespace is available as the @{namespace} and @{namespacePath} template variable
"l0.ASSWipe": { ... },
"l0.Nudge": { ... }
},
"modules": {
// Your modules go here. If your feed doesn't track any modules, you may omit this section (same goes for the macros object)
"l0.ASSFoundation": { ... }
}
An automation script or module object looks like this:
"l0.ASSWipe": {
"url": "@{baseUrl}#@{namespace}",
"author": "line0",
"name": "ASSWipe",
"description": "Performs script cleanup, removes unnecessary tags and lines.",
// These script information fields should be identical to the values defined in your
// DepedencyControl version record.
"channels": {
// a list of update channels available for your script (think release, beta and alpha).
// The key is a channel name of your choice, but should make sense to the user picking one.
"master": {
// This example only defines one channel, which is set up to track
// the HEAD of a GitHub repository.
"version": "0.1.3",
// The current script version served in this channel.
// Must be identical to the one in the version record.
"released": "2015-02-26",
// Release date of the current script version (UTC/ISO 8601 format)
"default": true,
// Marks this channel as the default channel in case the user doesn't have picked a specific one.
// Must be set to true for **exactly** one channel in the list.
"platforms": ["Windows-x86", "Windows-x64", "OSX-x64"]
// Optional: A list of platforms you serve builds for. You should omit this property for regular scripts
// and modules that use only Lua/Moonscript and no binaries. If this property is absent,
// the platform check will be skipped. The platform names are derived from the output of
// ffi.os()-ffi.arch() in luajit.
"files": [
// A list of files installed by your script.
{
"name": ".lua",
// the file name relative to the path assigned to the script by your namespace choice
// (see 3. Namespaces and Paths for more information). Available as the @{fileName} template variable
// for use in the url field below.
"url": "@{fileBaseUrl}@{fileName}",
// URL from which the **raw** file can be downloaded from (no archives, no javascript
// redirects, etc...). In this case the templates expand to
// "https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/l0.ASSWipe.lua"
"sha1": "A7BD1C7F0E776BA3010B1448F22DE6528F73B077"
// The SHA-1 hash of the file being currently served under that url. Will be checked
// against the downloaded file, so it must always be present and valid or the update process
// will fail on the user's end.
},
{
"name": ".lua",
"type": "test",
// Optional, defaults to "script". Specify "test" to denote a unit test.
// Currently only "script" and "test" are available, unknown script types will be skipped.
"url": "@{fileBaseUrl}.Tests.lua",
"sha1": "27745AB9CF04A840CF3454050CA9D38FA345CEBB"
},
{
"name": ".Helper.dll",
"url": "@{fileBaseUrl}@{fileName}",
"sha1": "0B4E0511116355D4A11C2EC75DF7EEAD0E14DE9F"
"platform": "Windows-x86"
// Optional. When this property is present, the file will only be downloaded to the users
// computer if his platform matches to this value.
}
],
"requiredModules": [
// an exhaustive list of modules required by this script. Must be identical to the required
// module entries in your DepdencyControl record, but you may not use short style here.
// (see 2. Usage for Automation Scripts for more information)
{
"moduleName": "a-mo.LineCollection",
"name": "Aegisub-Motion (LineCollection)",
"url": "https://github.com/torque/Aegisub-Motion",
"version": "1.0.1",
"feed": "@{feed:a-mo}"
},
{
"moduleName": "l0.ASSFoundation",
"name": "ASSFoundation",
"url": "https://github.com/TypesettingTools/ASSFoundation",
"version": "0.1.1",
"feed": "@{feed:ASSFoundation}"
},
{
"moduleName": "aegisub.util"
},
]
}
},
"changelog": {
// a change log that allows users to see what's new in this and previous versions. The changelog
// is shared between all channels. Only the entries with a version number equal or below
// the version the user just updated to will be displayed.
"0.1.0": [
"Sync with ASSFoundation changes",
// one entry for each line
"Start versioning with DependencyControl"
],
"0.1.3": [
"Enabled auto-update using DependencyControl",
"Changed config file to \\config\\l0.ASSWipe.json (rename ASSWipe.json to restore your existing configuration)",
"DependencyControl compatibility fixes"
]
}
}
To make maintaining an update feed easier, you can use several template variables that will be expanded when used inside string values (but not Keys).
Regular Variables: These reference a specific key or value and are available at the same depth and further down the tree from the point on where they were created.
Variables extracted at the same depth are expanded in a specific order. As a consequence only references to variables of lower order are expanded in values that are assigned to a variable themselves.
Depth 1: Feed Information
- feedName: The name of the feed
- baseUrl: The baseUrl field
- feed:###: A reference to a feed URL in the knownFeeds table
Depth 3: Script Information
- namespace: the script namespace
- namespacePath: the script namespace with all
.
replaced by/
- scriptName: the script name
Depth 5: Version Information
- channel: the channel name of this version record
- version: the version number as a SemVer string
Depth 7: File Information
- platform: the platform defined for this file, otherwise an empty string
- fileName: the file name
"Rolling" Variables: These variables can be defined at any depth in the JSON tree and are continuously expanded using the variables available. You can reference a rolling variable in itself, which will substitute the template for the contents the variable had at the parent-level.
Right now there's only one such variable: fileBaseUrl, which you can use to construct the URL to a file using the template variables available.
For an example to serve updates from the HEAD of a GitHub repository, see here. An example that shows a feed making use of tagged releases is also available.
This section is currently both incomplete and outdated. Sorry about that.
DependencyControl{tbl [requiredModules]={}, str :name=script_name, str :description=script_description, str :author=script_author, str :url, str :version, str :moduleName, str [:configFile], string [:namespace]} --> obj DependecyControlRecord
The constructor for a DepedencyControl record. Uses the table-based signature. Arguments:
-
requiredModules: the first and only unnamed argument. Contains all required modules, which may be either a single string for a non-version-controlled requirement or a table with the following fields:
- str [moduleName/[1]]: the module name
- str [version]: The minimum required version of the module. Must conform to Semantic Versioning standards. The module in question must contain a DependencyControl version record or otherwise compatible version number.
- str [url]: The URL of the site where the module can be downloaded from (will be shown to the user in error methods).
- str [feed]: The update feed used to fetch a copy of the required module when it is missing from the user's system.
- bool [optional=false]: Marks the module as an optional requirement. If the module is missing on the user's system, no error will be thrown. However, version requirements will be checked if the module was found.
- str [name]: Friendly module name (used for error messages).
-
name, description, author: Required for modules, pulled from the script_ globals for macros.
-
version: Must conform to Semantic Versioning standards. Labels and build metadata are not supported at this time
-
moduleName: module name (as used in require statements). Required for modules, must be nil for macros. Represents the namespace of a module.
-
url: The web site/repository URL of your script
-
feed: The update feed for your script.
-
configFile: Configuration file base name used by the script. Defaults to the namespace. Used for configuration services and script management purposes.
:checkVersion(str/num version, str [precision = "patch"]) --> bool moduleUpToDate, str error
Returns true if the version number of the record is greater than or equal to version. Reduce the precision to minor
or major
to also return true for lower patch or minor versions respectively. If the version can't be parsed it returns nil and and error message.
:checkOptionalModules(tbl modules) --> bool result, str errorMessage
Returns true if the optional modules have been loaded, where modules is a list of module names. If one or more of the modules are missing it returns false and an error message.
:getConfigFileName() --> str fileName
Returns a full path to the config file proposed for this script by DependencyControl. Uses the configFile argument passed to the constructor which defaults to the script namespace. The path is subject to user configuration and defaults to "?user\config". The file ending is always .json, because why would you use any other format?
The rationale for this function is to keep all macro and module configuration files neatly in one spot and make them discoverable for other scripts (through the DepedencyControl config file).
:getConfigHandler([defaults], [section], [noLoad]) => obj ConfigHandler
Returns a ConfigHandler (see ConfigHandler Documentation) attached to the config file configured for this script.
:getLogger(tbl args) => obj Logger
Returns a Logger (see Logger Documentation) preconfigured for this script. Trace level and config file preference default to user-configurable values. Log file name and prefix are based on namespace and script name.
:getVersionNumber(str/num versionString) --> int/bool version, str error
Takes a SemVer string and converts it into a version number. If parsing the version string fails it returns false and an error message instead.
:getVersionString(int [version=@version]) --> str versionString
Returns a version (by default the script version) as a SemVer string.
:getConfigFileName() --> str configFileName
Generates and returns a full path to the registered config file name for the module.
:loadConfig(bool [importRecord], bool [forceReloadGlobal]) --> bool shouldWriteConfig, bool firstInit
Loads global DependencyControl and per-script configuration from the DepedencyControl configuration file. If importRecord is true, the version record information of a DependencyControl record will be (temporarily) overwritten by the values contained in the configuration file. Global configuration is only loaded on first run or if forceReloadGlobal is true.
The first return result indicates there are changes to be written to the config file, the second result returns true if the config file was only just created. Intended for internal use.
:loadModule(tbl module, bool [usePrivate]) --> tbl moduleRef
Loads and returns single module and only errors out in case of module errors. Intended for internal use. If usePrivate is true, a private copy of the module is loaded instead.
:moveFile(str src, str dest) --> bool success, str error
Moves a file from source to destiantion (where both are full file names). Returns true on success or false and error message on failure.
:register(tbl selfRef, extraUnitTestArgs...) --> tbl selfRef
Replaces dummy reference written to the global LOADED_MODULES table at DependencyControl object creation time with a reference to this module. Also automatically registers unit tests for this module, passing in any extraUnitTestArgs
The purpose of this construct is to allow circular references between modules. Limitations apply: the modules in question may not use each other during construction/setup of each module (for obvious reasons).
Call this method as replacement for returning your module.
:registerMacro(str [name=@name], str [description=@description], func processing_function, func [validation_function], func is_active_function, bool|string [submenu=false])
Alternative Signature:
:registerMacro(func processing_function, func [validation_function], func is_active_function, bool|string [submenu=false])
Registers a single macro using script name and description by default.
Use submenu to specify a submenu name to use for this macro or set it to true
to use the automation script name.
If the script entry in the DependencyControl configuration file contains a customMenu property, the macro will be placed in the specified menu. Do note that that this setting is for user customization and not to be changed without the user's consent.
For the other arguments, please refer to the aegisub.register_macro API documentation.
:registerMacros(tbl macros, bool|string [submenuDefault=true])
Registers multiple macros, where macros is a list of tables containing the arguments to a :registerMacro() call for each automation menu entry. a single macro using script name and description by default.
Use submenuDefault to specify a submenu all macros will be placed in unless overriden on a per-macro basis. Defaults to true
which causes the automation script name to be used as the submenu name.
:registerTests(unitTestArgs...)
Registers unit tests for automation modules, passing in any of specified unitTestArgs. Registration of modules is done automatically upon calling :register
:requireModules([modules=@requiredModules], bool [forceUpdate], bool [updateMode], tbl [addFeeds={@feed})] --> ...
Loads the modules required by this script and returns a reference for every requirement in the order they were supplied by the user. If an optional module is not found, nil is returned.
The updater will try to download copies of modules that are missing or outdated on the user's system. The addFeeds parameter can be used to supply additional feeds to search. If missing/outdated requirements can't be fetched, the method will throw an error in normal mode or false and an error message in update mode.
Use forceUpdate to override update intervals and perform update checks for all required modules, even if requirements are satisfied.
:writeConfig(bool [writeLocal=true], bool [writeGlobal=true], bool [concert]]
Writes global and per-module local configuration. If concert is true, concerted writing will be used to update the configuration of all DependencyControl hosted by any given macro/environment at once. See ConfigHandler documentation for more information. Intended for internal use.
:getUpdaterErrorMsg(int [code], str targetName, ...) --> str errorMsg
Used to turn an updater return code into a human-readable error message. The name of the updated component and other format string parameters are passed into the function.
VarArgs:
- bool isModule: True when component is a module, false when it is an automation script/macro
- bool isFetch: True when we are fetching a missing module, false when updating
- extError: Extended error information as returned by the :update() method
:getUpdaterLock(bool [doWait], int [waitTimeout=(user config)]) --> bool result, str runningHost
Locks the updater to the current macro/environment. Since all automation scripts load in parallel we have to make sure multiple automation scripts don't all update/fetch the same dependencies at once multiple times. The solution is to only let one updater operate at a time. The others will wait their turn and recheck if their required modules were fetched in the meantime.
If doWait is true, the function will wait until the updater is unlocked or waitTimeout has passed. It will then get the lock and return true. If doWait is false, the function will return immediately (true on success, false if another updater has the lock). Intendend for internal use.
:releaseUpdaterLock()
Makes an updater host (macro) release its lock on the Updater if it has one. See :getUpdaterLock for more information
:update(bool [force], tbl [addFeeds], bool [tryAllFeeds=auto]) --> int resultCode, str extError
Runs the updater on this automation script or module. This includes recursively updating all required modules. When force is true, required modules will skip their update interval check.
By default, the updater will process all suitable feeds until one feed confirms the script to be up-to-date (unless configured otherwise by the user or if we are looking for updates to an outdated component). Set tryAllFeeds to true to check all feeds until an update is found. You can also supply additional candidate feeds.
Returns a result code (0: up-to-date, 1: update performed, <=-1: error) and extended error information which can be fed into :getUpdaterErrorMsg() to get a descriptive error message.
tbd
tbd
tbd
Reference documentation for the UnitTestSuite module is available in the source code
tbd