Skip to content
Phil Gaiser edited this page Apr 1, 2024 · 32 revisions

Addons

The Project Init system is extensible by an add-on mechanism. You can create your own custom addon resource to include new project types for a particular language, available for the user to select when he wants to create a new project for that language. You can also include entirely new programming languages which are not already supported by the base system. On top of that, you can also change most of the appearance and branding to fit your needs. For example, you may change the start icon, title and description which are shown when Project Init is run. Certain system-, project- and language-specific parameters are configurable via a properties file. You can define your own shell scripts as hooks that get automatically executed before and/or after a new project is initialized by a user.

An addon resource is a directory containing the files that make up your addon. It is only possible to use one particular addon resource at a time. Therefore, if you have multiple addons, you either have to combine the files into one addon resource directory or explicitly choose which addon to use before you run Project Init.

Setup

Setting up an addon resouce is fairly simple. Create a directory in your filesystem where your addon resources should reside. Add an empty file named 'INIT_ADDONS' in that directory. This is necessary to mark a directory as a Project Init addon resource. Without that file you will get a warning when you run Project Init with addons enabled. Lastly, in order to inform the Project Init system that this particular directory should be used as an addon resource, you must set the environment variable $PROJECT_INIT_ADDONS_RES to the path to that directory.
For example, if you want your addon resources to reside in the /home/myuser/somedir/myaddon directory, you must set the environment variable $PROJECT_INIT_ADDONS_RES to point to that directory, e.g. with:

export PROJECT_INIT_ADDONS_RES="/home/myuser/somedir/myaddon";

If you want the environment variable to be set automatically every time you open a new interactive shell, then add the above line to your user's .bashrc file.

Since v1.2.0 you can also specify the addon to be used by a configuration file in your user's HOME directory. To do that, put the value of the $PROJECT_INIT_ADDONS_RES environment variable in the project-init-addons-res file instead. The file must be placed in your user's HOME directory. You can precede the filename with a '.' (dot) to make the file hidden.
If the project-init-addons-res configuration file is present, it takes precedence over the $PROJECT_INIT_ADDONS_RES environment variable.

That's it. Your set up directory will now be considered by the Project Init system.

Use a Git Repository

A Project Init addon does not necessarily have to be located in your local filesystem. You can also put your addon resources in a Git repository. The rules for the content of the Git repository are the same as with a local directory. There is no added functionality with addons in a Git repository. It just makes it simpler to organise and share your files, particularly if you are using your Project Init addon in the context of an organisation. When you specify an addon as a Git repository, the system will simply clone that repository via the specified protocol (HTTPS or SSH) in a temporary directory and then treat that directory as if you had specified it as the addon resource.

In order to use a Git repository as an addon resource, set the $PROJECT_INIT_ADDONS_RES environment variable or project-init-addons-res configuration file to the value that you would pass to the git clone command if you were to clone that repository manually. In order to mark the addon resource as a Git repository, the environment variable value or configuration file content must start with 'GIT:' followed by the remote URL.
For example, if you put your repository on GitHub, you might set the $PROJECT_INIT_ADDONS_RES environment variable to something like this:

export PROJECT_INIT_ADDONS_RES="GIT:[email protected]:my-user/my-addon-repo.git";

Or equivalently, put the content in the project-init-addons-res configuration file:

GIT:[email protected]:my-user/my-addon-repo.git

By default, the set default branch of your Git repository is used when cloning it. If you want to use a different branch or a specific tag, you can set the $PROJECT_INIT_ADDONS_RES_BRANCH environment variable to the desired branch or tag:

export PROJECT_INIT_ADDONS_RES_BRANCH="my-branch-or-tag";

Since v1.2.0 you can also specify the branch or tag by a configuration file in your user's $HOME directory. To do that, put the value of the $PROJECT_INIT_ADDONS_RES_BRANCH environment variable in the project-init-addons-res-branch file instead. The file must be placed in your user's $HOME directory. You can precede the filename with a '.' (dot) to make the file hidden.
If the project-init-addons-res-branch configuration file is present, it takes precedence over the $PROJECT_INIT_ADDONS_RES_BRANCH environment variable.

Since v1.2.0 we recommend using configuration files if possible, but the use of environment variables is still supported. If you decide to use environment variables, we recommend to set them in your user's .bashrc file so that you don't have to manually set them every time you want to use Project Init. For a multi-user system you could alternatively set the environment variables in the /etc/environment system file.

Configuration

Certain system-, project- and language-specific parameters can be configured in a central configuration file. The base project.properties defines all available properties and their default values. If you want to override one or more properties create a file named 'project.properties' in the root of your addon resource directory and redefine the corresponding properties to the new values.

For more information, an example and how to query project properties from your addon code, please see the Configuration Section.

Using the API

When creating addons for the Project Init system, it is advisable, just like with any other software component, to only use public APIs. The API is well documented in the API Reference document. As long as your addon depends only on public APIs, it is guaranteed to be compatible to the base Project Init system according to the common semantic versioning specification.

Variable Substitution

You can introduce new substitution variables in your addons. The principle is the same as with all substitution variables already provided by the base system. Your substitution variables must conform to the pattern ${{VAR_?}} where the '?' is the identifying name of the corresponding substitution variable. When introducing a new substitution variable, make sure the identifying name is not already used.

You should replace your substitution variables by calls to the replace_var() function. Usually, these calls are put inside the corresponding process_files_lvl_X() callback function, where the X is the integer number of the init level that the function is responsible for. This ensures that the substitution variables are replaced at the right time in the process.

As an example, let's say that you have an addon that introduces a new substitution variable named ${{VAR_MY_NEW_THING}}. You want to substitute the variable with the value provided by a user prompt in your init script at init level 2. Your init script could look something like this:

# Your init script at init level 2

function process_files_lvl_2() {
  replace_var "MY_NEW_THING" "$var_my_new_thing";
}

# [...]

logI "Enter the name for your new thing:";
read_user_input_text;
var_my_new_thing="$USER_INPUT_ENTERED_TEXT";

# [...]

# Let's say that there is a next init level
proceed_next_level "$next_dir";

One important thing to note is that the Project Init system does not scan all copied project source template files by default. This means that you might have to register the file(s) where you want to use your new substitution variable with the system. This is done by placing a text file named 'files.txt' in the root of your addon resource directory and adding a line in that file with the filename of the project source template file(s) you want to scan for substitution variables. You can also use wildcard characters ('*') to specify file patterns. You only have to do this, though, for files that are not already considered by the Project Init system. Take a look at the base files.txt file to see what filenames and patterns are already used.

Variable Substitution Files

Besides directly specifying the substitution variable value in the init code when calling the replace_var() function, you can also place the variable value in a file within your addon resource directory. This is advisable, for example, when the variable value has a lot of content/text and you don't want to clutter your init script with that. Additionally, it allows your addon to replace a substitution variable dynamically by a different value depending on the init level. So a sub-init-level may override what value is effectively used for a specific substitution variable, overriding any of its parent init levels. This is because the load_var_from_file() function traverses all init levels in reverse order, starting from the lowest (actively used) init level going backwards to the root while only considering the first found variable file.

In order to place a substitution variable's value in a file, you must decide at which init level that substitution variable should be replaced. Then create a 'var' subdirectory there and place the substitution variable value in a new file within that 'var' subdirectory. The name of the file should be the same as the substitution variable name, in all lower-case and without the 'var_'-prefix. The file should not have a file extension.
Then in your init script at that level you just have to call the load_var_from_file() function to replace the substituion variable. Usually, this is done in the process_files_lvl_X() callback function as shown in the last section.

Since v1.3.0 it is deprecated behaviour to place the substitution variable files directly in an init level directory. It is still supported but will show a deprecation warning. If your addon still relies on this behaviour, then please move the substitution variable files in a 'var' subdirectory under the init level where they reside and remove any 'var_'-prefix and '.txt'-suffix from the file name. Please also make sure that your addon's init code uses the load_var_from_file() function instead of the deprecated load_var() function.
Please note that this deprecated behaviour also applies when using the replace_var() function, since it tries to load the substitution variable value from a var file when only the key argument is specified. If you get a deprecation warning with your addon, then move your var files in a subdirectory and rename them as described above.

Shared Source Templates

Since v1.3.0
Project source templates that are used in more than one place can be put in a central location to make it easier to maintain. When source templates are shared, their content can be included in other source templates by using an include directive.
In order to share a source template file, you must create a 'share' subdirectory under the root of your addon resource. All files under that directory are automatically shared across all project types. It is possible to create a deeper subdirectory structure inside that directory to better organise the shared files. Then inside other project source template files, any shared template file may be referenced by using an include directive. An include directive looks like this ${{INCLUDE:?}}, where the '?' is the path to the file to include inside the 'share' subdirectory.
An include directive can be placed anywhere in a source template file, however, it must always be on a separate line without any other text to the left or right of it, otherwise it is ignored.

For example, let's say that you have a file which you would like to use in every project type as it contains some code or other information that is common to all of your projects. Let's say that you call that file 'myCommonFile.txt'. Then place it under the 'share' subdirectory within your addon. Then in the sources of your project types where you want to include that file, also create a 'myCommonFile.txt' file but instead of copy-pasting the content there, just write a single line: ${{INCLUDE:myCommonFile.txt}}
When the underlying project is initialized, the 'myCommonFile.txt' file will then contain the content of the shared template.

If the inclusion of some shared template file in an initialized project depends on some condition, then it is also possible to create the corresponding logic in code inside an init script and use the copy_shared() function to perform the copying of the shared resource when appropriate.

Declaring a Dependency

If your addon code uses and depends on an external system command (i.e. a command that is not a Bash builtin), then you might want the Project Init system to check whether that command is avaialable. This is sensible, for example, if you manage a Project Init addon for your organisation and you have many users potentially using different Linux distributions and versions and you want to make sure that the utility program that your addon code depends on is available on all systems.

Before you go and declare your own dependencies, please check the base dependencies.txt file to see if your dependencies have already been declared. For example, if you use awk or grep in your addon code, then you don't have to declare them again as the system already depends on them.

If your dependency is not already included in the base 'dependencies.txt' file, then create a file named 'dependencies.txt' in the root of your addon resource directory. Each additional dependency is put on a separete line. Use the command name by which you want to call the external program in your addon code. Empty lines are allowed. Lines denoting comments must start with a '#' character.
For example, if you want to use the external command 'mycmd' in your addon code, then you could add the following content to your 'dependencies.txt' file:

# This is a comment
mycmd

# Another comment

By default, if the Project Init system cannot find any declared command, the program will refuse to proceed and exit with a failure message. You can override this behaviour by redefining and setting the 'sys.dependencies.onmissing.fail' system property to 'false'.
That is, add the following line to the 'project.properties' file of your addon resource:

sys.dependencies.onmissing.fail=false

Please note that a user will still get a warning message for a missing dependency but the Project Init system will otherwise proceed normally. If you are the author of the addon resource, it is your responsibility to ensure that your addon code safely handles the case of a missing dependency.

Loading Common Code

Since v1.5.0
Your addon might have code which is common to all init scripts and Quickstart functions. If that's the case, then it makes sense to place that code into a separate library script file to make the addon more maintainable. You can then instruct Project Init to automatically load the code in that library file once the addon resource has loaded. To declare such a script file, add the following line in the project.properties configuration file of your addon:

sys.addon.code.load=mylibs.sh

This will instruct Project Init to load the script file mylibs.sh into the global namespace once your addon resource becomes available. The property value is interpreted as a path, relative to your addon's source tree root. Since the file is directly loaded, i.e. sourced, into the global namespace, care must be taken by an addon not to cause any collisions of symbol names. Since the script file is sourced, the shell code inside it is executed. We therefore highly recommend to only place functions and reused global variables inside such a file and to generally treat it as a common library file for your addon.

Appearance

Your addon can change some aspects of the appearance of the Project Init program. Mainly the icon and text that is shown at the beginning can be adjusted to fit your brand and information.

Icon

To change the icon that is shown at startup, add a text file named 'icon.ascii.txt' to the root of your addon resource directory. The content of that file could represent some ASCII-art of your choice or the text representation of your organisation's logo. Please note that the content of the icon file will simply be read and displayed to the user as is. Concretely, each line of the icon file is printed to the user's screen by means of the logI() function. Therefore, you could for example use control characters for ANSI-colours and Unicode-smileys to spice up your icon. But please note that we make no statement about whether the terminal of the underlying user supports these features. We therefore generally recommend to stick to plain ASCII characters.

If you want to completely disable the start icon, you can add the following line to your addon 'project.properties' file:

sys.starticon.show=false

Title

To change the title that is shown at startup, add a text file named 'title.txt' to the root of your addon resource directory. The content of that file is displayed to the user as is.

If you want to completely disable the start title, you can add the following line to your addon 'project.properties' file:

sys.starttitle.show=false

Description

To change the description text that is shown at startup, add a text file named 'description.txt' to the root of your addon resource directory. The content of that file is displayed to the user as is.

If you want to completely disable the start description, you can add the following line to your addon 'project.properties' file:

sys.starttext.show=false

Notification Icon

Since v1.1.0
You can change the icon within the desktop notification that is shown when a project is successfully initialized. By default, the standard icon representing the used programming language is used. Your addon can both override this globally or individually for one or more specific languages. The notification icon is defined by an image file in your addon resource. The file must always be named 'icon.notif.png', even when defining multiple different icons in separate directories for different situations. We recommend using a 48x48 PNG image.

If you want to show a custom icon in the desktop notification regardless of the used programming language of the project, that is you always want your custom icon to be used, then place your custom icon directly in the root of you addon resouce.
If your addon provides a new programming language or you want to redefine the icon of an already supported language, you only have to place the 'icon.notif.png' image file in the applicable subdirectory. For example, if you want to redefine the icon when initializing C++ projects, your image file would go to 'your_addon_res/cpp/icon.notif.png'.

If you want to disable desktop notifications entirely, you can add the following line to your addon 'project.properties' file:

sys.notification.success.show=false

Success Message

Since v1.1.0
You can adjust the message shown when a new project is successfully initialized. This both affects the message printed in the terminal as well as the description text in the desktop notification (if enabled). To change the text, set the PROJECT_INIT_SUCCESS_MESSAGE global variable in any of your addon's init scripts to the message of your choice.

Versioning

You can specify the version of your addon in a separate file. When this file is present, the Project Init system will read it and show your addon's version to the user if the default title text is used. To specify the version of your addon, create a file named 'VERSION' in the root of your addon resource directory. The file must contain one text line denoting the version of your addon in the format 'major.minor.patch', where 'major', 'minor' and 'patch' are the corresponding version numbers. You may optionally use a '-dev' postfix to indicate that it is a development version.

For example, you could specify the version of your addon by adding the following line to the 'VERSION' file:

1.2.3

or if it is a development version:

1.2.3-dev

Hooks

Hooks can be used by addons to run arbitrary Bash shell code both at the beginning of the project initialization and at the end after a project was successfully initialized. The two types of hooks, called a load-hook and after-init-hook, respectively, are represented by executable shell script files in the root directory of the addon.

Note: Since v1.2.0 a warning is shown if the shell script of any hook exits with a non-zero exit status.

Load-Hook

The load-hook is executed by the Project Init system after the program has successfully started but before the main form is shown to the user to let him enter his parameters for the new project. In fact, the load-hook is executed even before the start icon is shown. An addon may use a load-hook to perform arbitrary work given the event that a user wants to initialize a new software project.

To add a load-hook to your addon, add a text file named 'load-hook.sh' to the root of your addon resource directory. Make sure the file is marked as executable. It will then be executed in a shell subprocess by the Project Init system once it has completely loaded. Please note that the script code of the load-hook does not have access to the Project Init API, nor to any defined global variable of the Project Init system. The current working directory will be set to the root directory of the addon resource. Both stdout and stderr will be redirected to /dev/null, so you cannot use a load-hook to print something to the user's terminal.

After-Init-Hook

The after-init-hook is executed by the Project Init system at the end after the program has successfully initialized a new project. An addon may use an after-init-hook to perform arbitrary work given the event that a user has initialized a new software project.

To add an after-init-hook to your addon, add a text file named 'after-init-hook.sh' to the root of your addon resource directory. Make sure the file is marked as executable. It will then be executed in a shell subprocess by the Project Init system after a new project was initialized. Please note that the script code of the after-init-hook does not have access to the Project Init API, nor to any defined global variable of the Project Init system except the $VAR_PROJECT_DIR global variable, which will be set to the path to the directory of the newly initialized project. The current working directory will be set to the root directory of the addon resource. Both stdout and stderr will be redirected to /dev/null, so you cannot use an after-init-hook to print something to the user's terminal.

User-Defined After-Init-Hook

Since v1.5.0
An after-init-hook which is specific to the underlying user and not part of an addon resource directly. This allows each user to specify his own hook which gets automatically executed after a new project is initialized. To declare your own after-init-hook, put the following line inside your own project.properties file, i.e. the one in your own home directory:

sys.user.hook.afterinit=myhook.sh

This will instruct Project Init to execute the myhook.sh shell script every time a new project is initialized. The property value is interpreted as a path, relative to your home directory. It must not denote an absolute path. The specified script must exist, it must be executable and it must be owned by your user. This does not necessarily require that an addon is active. Your user-defined hook is independent from any addon. In the case an addon is active and has also declared an after-init-hook, your user-defined hook is executed after the one from the addon.

The script of your hook will be executed in a shell subprocess by the Project Init system. The script code does not have access to the Project Init API, nor to any defined global variable other than the $VAR_PROJECT_DIR global variable, which will be set to the path to the directory of the newly initialized project. Additionally, the $PROJECT_INIT_QUICKSTART_REQUESTED global variable will be available to your hook and set either to 'true' or 'false', depending on whether the user has requested a Quickstart or not. In the case of a Quickstart, the $VAR_PROJECT_DIR global variable will be set to the user's current working directory when he used the Quickstart. The current working directory of the executed hook will always be set to your user's $HOME directory. Both stdout and stderr will be redirected to /dev/null, so you cannot use your custom after-init-hook to print something to the user's terminal.

Add Custom Licenses

Adding additional licenses the user can choose from is quite easy. In the root of your addon resource directory create a subdirectory named 'licenses'. Then for each additional license, create a subdirectory under 'licenses'. You can name the subdirectories whatever you want. The order of the licenses as displayed in the selection list of the prompt shown to the user is determined by the directory names.

In the subdirectory of your custom license you have to add a couple of files to make it usable. First, add a text file named 'name.txt'. It should contain one line denoting the human-readable name of the custom license. This name is used in the selection list of the user prompt.
Secondly, add a text file named 'license.txt'. It should contain the legal license text of your custom license. This file will essentially be copied to the project target directory of a newly initialized project that uses your custom license.
Lastly, you have to specify how the source code file copyright headers should look like. For that purpose, add a text file named 'header.?' for every applicable type of source code file, where '?' is the corresponding applicable file extension. For example, if you want to define the copyright header used in all Java source code files (i.e. files ending with '.java'), then create a file named 'header.java' and add the corresponding copyright header text.
You can use substitution variables in the copyright header text. For example, it is common to use the substitution variables ${{VAR_COPYRIGHT_YEAR}} and ${{VAR_COPYRIGHT_HOLDER}} to indicate the year and owner of the copyright notice. You don't have to process these substitution variables manually in your addon code. This is handled automatically for you by the project_init_license() function.

It is common to only specify copyright header files for major programming- and markup language extensions. But sometimes there are situations where a different file extension is used for certain source code files, even though the format closely matches the one from a major programming language. In that situation you can instruct the Project Init system to use the copyright header for a specific file extension in the case it encounters a different specific file extension. This is essentially a unidirectional mapping from one file extension to another. To see what mappings already exist in the base system, inspect the base extension_map.txt file. You only have to create a mapping entry if it does not already exist in the base system. To do that, create a file named 'extension_map.txt' in the 'licenses' subdirectory of your addon resource. For each mapping, add a line mapping a key to a value separated by a '=>' sign.
For example, the following line would instruct the Project Init system to use the copyright header as specified in the header.xml file when replacing the ${{VAR_COPYRIGHT_HEADER}} substitution variable in project template source files ending with '.html' (i.e. use the same copyright header for HTML files as for XML files):

html => xml

Lines denoting comments must start with a '#' character.

Add Custom Project Types

Your addon can extend the Project Init system to add more project types for already supported programming languages. For that you basically mirror the directory layout of the base system. For example, if you want to add an additional custom project type for Java projects (Java is already supported by Project Init), then simply create a subdirectory named 'java' in the root of your addon resource directory. Then in that subdirectory add another subdirectory for each additional custom project type. The name of those subdirectories can be whatever you want. However, the name will determine the position of the corresponding project type item in the selection list of the user prompt.

All custom project type subdirectories should contain a 'name.txt' file with one line denoting the human-readable name of the corresponding project type. This is the name as shown to the user in the selection list.

In order for your custom project type to be recognised by the system, you must provide an init script for it. To do that, add a file named 'init.sh' to the project type subdirectory. The code in that init script is automatically executed when the user selects your custom project type for initialization. It then takes control and has to proceed with the initialization like any other init script.

Add Custom Programming Languages

Your addon can extend the Project Init system to add more programming languages which are not already supported by the base system. The way this works is essentially the same as with supported base languages. Simply create a subdirectory for your custom programming language in the root of your addon resource directory. For example, if you want to add support for TypeScript projects, create a subdirectory named 'ts'. In that subdirectory, create a text file named 'name.txt' and add a single line to it denoting the human-readable name of the custom programming language. This name will be used by the system when displaying your custom language in the selection list.

The subdirectory must be an init level directory. Therefore, you must provide an 'init.sh' init script. The code in this init script will be executed by the system when the user selects your custom programming language in the shown selecion list.
As with programming languages supported by the base system, you must provide at least one project type. Therefore, your custom language subdirectory must have at least one subdirectory containing the init level code and content for that project type.

In init script for init level 1 you can use the select_project_type() API function to have the Project Init system automatically search for project types and show the corresponding selection list to the user.

Quickstart Functions

Since v1.4.0
An addon can define its own Quickstart functions. All Quickstart functions must be defined in a file quickstart.sh in the source root of your addon resouce. Your addon can define as many function there as it wants, including internal auxiliary functions. To mark a function as Quickstart-usable, the function name must start with the prefix 'quickstart_', followed by an arbitrary Quickstart name. When specifying that function as an argument to the project-init command, the 'quickstart_'-prefix is omitted. Users must precede the Quickstart name with an '@' character in the command argument.

For example, in the quickstart.sh file of your addon, you can place the following function definition:

function quickstart_my_example_fn() {
  logI "Running the example Quickstart function";
  return $QUICKSTART_STATUS_OK;
}

That function is called by Project Init when you do:

project-init @my_example_fn

The name of a Quickstart function definition should only consist of lowercase letters and underscores. However, when specifying the name as an argument to the project-init command, a user can also use forward slahes and dots while the entire name is case insensitive. This is convenient for example in the case where a Quickstart function only generates a single source file because a user can specify the Quickstart name argument like a file name with a file extension.

The returned status code of a Quickstart function tells the system whether the function succeeded or failed. A successful Quickstart function should return 0 (zero), whereas a failed Quickstart function should return a non-zero status code. You may use either the QUICKSTART_STATUS_OK or QUICKSTART_STATUS_FAILURE constants as return values. If a Quickstart function signals a failed operation, all files generated by it will be discarded by the system.

A Quickstart function can do pretty much anything it wants. Most of the base functionality provided by the Project Init tool is also available in Quickstart functions. All symbols that are defined during the execution of an init script are also available in Quickstart functions, however, keep in mind that not all API functions can be called since it does not make sense for all of them to be used in the context of a Quickstart function. You can see some example code in the quickstart.sh script provided by the addons example resources.
Please note that when a Quickstart function is invoked, the current working directory is not the same as the user's current working directory who used the command. So when doing file operations, both with the provided Project Init API functions or manually, you must use the USER_CWD global variable to get the path to the current working directory of the underlying user.

Testing

Since v1.2.0
Addons can use the functionality test API to automate testing. This previously internal API was published and made available to addons in version 1.2.0 of Project Init. Each test case will validate that the functionality of a given addon is as expected. To facilitate this, you have to provide the test parameters for each test case in a specific file and then instruct the Project Init tool to execute a test run with those parameters. The parameters are basically the answers that a normal user would provide for the questions in the form of the program. The tool will then run through normally with the supplied answers and initialize the corresponding concrete project in a temporary location. You can then perform some checks in the code of your test case to make sure that the initialized project actually contains certain files and folders.

Tests Setup

Setting up testing for your addon is very easy as the process is well integrated into the Project Init tool.
In the source root of your addon, create a tests directory. This is where all of your testing-related files will be placed.

For each test case you have to create two files. A test case script that implements at least one specific testing function and a properties file that contains the parameters for the specific test case.

The test case script must be named test_func_?.sh, where the '?' is the name of the test case. You can theoretically pick any name you want but we recommend that you organise your test case names according to the directory structure of the underlying project type that the specific test case covers. For example, if you have read the tutorial for project types, then in the given tutorial example project type you could name your test file test_func_java_example_app.sh.

Each test case script file must also be accompanied by a properties file defining the test parameters. These files must be placed inside the tests/resources directory. By convention, the file name is similar to the name of the related script file but it has the pattern test_?.properties instead, where the '?' is the name of the test case. When testing Quickstart functions, a test case parameter file is optional of the underlying Quickstart function does not use any form inputs.

Lastly, in order to execute the entire test suite for your addon, you have to call the test.sh control script of the Project Init tool and ensure that the path specified by the --test-path option points to your addon's source root. We provide an official addon test.sh control script to do all of this for you automatically. To use it, simply copy this test.sh control script to your addon's source root. Then, whenever you want to run your addon's test suite, you just execute that test.sh scipt from your source root.

Test Case Script

Placing a test case script file with the correct name pattern (see previous section) inside your addons tests directory will allow the Project Init testing facility to automatically pick it up during testing. Each test case script must define at least the test_functionality() function. This is the entrypoint for your functionality test case. That function will be automatically called by the testing facility.

Theoretically, you could do just about anything inside the test_functionality() function. The return value determines whether the functionality test run is successful or not. The function should return 0 (zero) for a successful test run (i.e. passed test), and non-zero if the test failed. So you could implement your own testing functions and capabilities in any way you want. Usually, however, the test_functionality() function would simply instruct the Project Init testing facilities to start a functionality test run using a specific properties file containing the test parameters. This can be done by calling the test_functionality_with() function and specifying the properties file to use as an argument. Since it also returns the status code of the executed test run (passed/failed), the test_functionality() function implementation for a test case usually just calls the test_functionality_with() function to execute a specific test run and then returns its exit code (see example below).

When calling the test_functionality_with() function, a complete test run is executed and all generated files are saved in a temporary location. By specifying the test parameters, the Project Init tool is able to go through the entire form in a specific path and initialize a new test project based on the provided test parameters (i.e. form answers) automatically just as if a real user was doing it manually by running Project Init with your addon enabled. This asserts that the underlying project type with the given parameters can actually be initialized without warnings and errors. Additionally, you may also want to assert in your tests case that certain files or directories are actually present or non-existent in the initialized ptoject. For that purpose, after the test project is initialized without any warnings and errors, the testing facility will call the test_functionality_result() function in your functionality test case script file. This is completely optional, so you don't necessarily have to define that function.
Inside the test_functionality_result() function, you can perform all the checks to ensure that the expected files are present in the initialized project. The Project Init testing facilities provide API functions to ensure file/directory existence/non-existence. You can create an array of filenames that you want to check and then pass this to one of the assert_files_exist(), assert_dirs_exist(), assert_files_not_exist() or assert_dirs_not_exist() functions. The return code of the test_functionality_result() function is used to determine whether the result tests are successful or not. The function should return 0 (zero) for a successful result test and non-zero if the result test failed.

With all the above information, you can create functionality test cases for your own addon. The overall structure of test cases looks fairly similar.

A typical test case script could look like this:

#!/bin/bash

#*****  Example Test Case  *****#


function test_functionality() {
  test_functionality_with "test_example.properties";
  return $?;
}

function test_functionality_result() {
  local files=();
  files+=("this_file_should_exist");

  local not_files=();
  not_files+=("this_file_SHOULD_NOT_exist");

  local dirs=();
  dirs+=("this_directory_should_exist");

  local not_dirs=();
  not_dirs+=("this_directory_SHOULD_NOT_exist");

  assert_files_exist "${files[@]}"         &&
  assert_files_not_exist "${not_files[@]}" &&
  assert_dirs_exist "${dirs[@]}"           &&
  assert_dirs_not_exist "${not_dirs[@]}";
  return $?;
}

Note: In all of your test case scripts, do not place any code outside of functions. Your test case scripts are sourced at an appropriate time automatically by the Project Init testing facilities. Hence, placing any code in the global scope will result in that code being executed directly when the test case script file is sourced. This can mess up the printed output of the test.sh script and even potentially render your test case useless. Always place your test case code in a function scope.

Test Case Parameters

Test case parameter files are not automatically recognised by the testing facility, instead they have to be explicitly specified as an argument by the test case script when it calls the test_functionality_with() function. A test case parameter file is a simple properties-formatted text file. Lines starting with '#' are ignored as comments. Each non-comment line specifies the value of a concrete test parameter as a property, i.e. key-value-pair. The first line of each test case parameter file may contain a special comment denoting the name of the test case run. This special comment must start with # @NAME: in order to be recognised as such. This is used to display a more descriptive name for the test run in the summary. If this special comment is not present in the test case parameter file, then the name of the file is used instead.

The key of each defined property corresponds to what would be assigned to the FORM_QUESTION_ID global variable right before emitting a form question to the user. The value of each defined property defines the answer to that form question when the test run is executed. The following table shows the question identifiers used for the main form questions usually shown at the beginning:

Question Identifier Description
project.name The name of the project
project.description The short description of the project
project.license The license of the project
project.dir The output directory where the project should be initialized in
project.language The programming language to use for the project
project.type The concrete project type to initialize based on the chosen programming language

If an init.sh script of your addon uses a form API function, then you can find the corresponding question identifier that is used by that function in its API documentation. If your addon defines its own functions for form questions, then make sure you set the FORM_QUESTION_ID global variable right before your init code asks the question to the user, so that your test cases can refer to that question by an identifier in a test case parameter file.

A typical test case parameter file could look like this:

# @NAME: addon Java example app
project.name=Test_Run
project.description=Functionality Test for the Addon Example App
project.license=My New License
project.dir=addons/04A_example_app
project.language=Java
project.type=My Example Application
java.version=Java 17

Test Configuration

Your test suite can run with an altered Project Init configuration. This can be useful to avoid certain behaviour that would not be appropriate to be included in a test case run. To change the configuration specifically used in test suites, place a project.properties file inside the tests/resources directory. That file is equivalent to the main configuration file of your addon (see section Configuration) but it is only loaded during test runs. It may override any configuration property, even if it was already overridden by your addon.

Testing Quickstart Functions

Quickstart functions can be tested in the same way as project setups by init scripts. But instead of the test_functionality_with() function, your test case code must use the test_functionality_quickstart() function and provide the name of the Quickstart to test. If the underlying Quickstart function uses inputs, the call to test_functionality_quickstart() must also specify a test case parameter file so that answers can be provided during the test run. You can see an example of a test case for a Quickstart function here.

Test Automation with GitHub Actions

If you place your addon in a repository on GitHub, regardless whether it's public or private, then you might want to have your functionality tests automatically run when someone pushes commits. Doing this with GitHub Actions is super easy. You can use the below workflow file to accomplish this. In you addon's repository simply create a file .github/workflows/test.yaml and copy the following content into it:

name: 'Test'
on:
  push:
    branches:
      - '**'

jobs:
  test-functionality:
    name: 'Addon Functionality'
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v4

      - name: 'Test Functionality'
        env:
          TERMINAL_NO_USE_CNTRL_CHARS: '1'
        run: ./test.sh