|
| 1 | +# Extension Development |
| 2 | + |
| 3 | +## Setup |
| 4 | + |
| 5 | +The basic setup of your project's directory is like this: |
| 6 | + |
| 7 | + <project>/ |
| 8 | + ├─ build/ |
| 9 | + ├─ docs/ |
| 10 | + ├─ src/ |
| 11 | + ├─ tests/ |
| 12 | + ├─ <info-files> |
| 13 | + └─ <config-files> |
| 14 | + |
| 15 | +### The `build` directory |
| 16 | + |
| 17 | +This directory is used to keep the build artifacts and reports that might be useful in additional steps. It is not tracked with the versioning system. |
| 18 | + |
| 19 | +### The `docs` directory |
| 20 | + |
| 21 | +Here you keep the documentation for your extension. |
| 22 | +The internal structure depends on how you organise it, and which tools you want to use for publishing. |
| 23 | + |
| 24 | +> A good tool for publishing is [Bookdown](http://bookdown.io/). Bookdown generates DocBook-like HTML output using CommonMark (a variation of Markdown) and JSON files instead of XML. Bookdown is especially well-suited for publishing project documentation to GitHub Pages. Bookdown can be used as a static site generator, or as a way to publish static pages as a subdirectory in an existing site. See also the [Docker Bookdown image with a collection of templates](https://hub.docker.com/r/sandrokeil/bookdown/). |
| 25 | +
|
| 26 | +### The `src` directory |
| 27 | + |
| 28 | +There are two different philosophies in the wild on how the `src` directory should be organised, the _package_ layout and the _runtime_ layout. Both have their pros and cons. |
| 29 | + |
| 30 | +#### Package Layout |
| 31 | + |
| 32 | +The source code has the structure as needed for the distribution package, for example |
| 33 | + |
| 34 | + src/ |
| 35 | + ├─ admin/ |
| 36 | + ├─ languages/ |
| 37 | + ├─ media/ |
| 38 | + ├─ site/ |
| 39 | + └─ manifest.xml |
| 40 | + |
| 41 | +While this layout makes packaging easier, it is harder to use it for integration tests. |
| 42 | + |
| 43 | +#### Runtime Layout |
| 44 | + |
| 45 | +The runtime layout uses the same directory structure as the targeted Joomla! version, for example |
| 46 | + |
| 47 | + src/ |
| 48 | + ├─ administrator/ |
| 49 | + │ ├─ components/ |
| 50 | + │ └─ languages/ |
| 51 | + ├─ components/ |
| 52 | + ├─ languages/ |
| 53 | + └─ media/ |
| 54 | + |
| 55 | +This layout allows you to embed your extension in a Joomla! installation during development. While this layout makes packaging harder, it is easier to use it for integration tests. |
| 56 | + |
| 57 | +### The `tests` directory |
| 58 | + |
| 59 | +The layout of the tests directory depends on the testing tool(s) you are using. In any case, you should differentiate at least three types of tests: |
| 60 | + |
| 61 | +- **Unit tests:** |
| 62 | + Tests that do not need a particular setup. |
| 63 | +- **Integration tests (edge-to-edge):** |
| 64 | + Tests that need access to Joomla classes, and maybe a database (preferably SQLite, as it does not need extra installation steps) |
| 65 | +- **Acceptance tests (end-to-end):** |
| 66 | + Tests that need the complete stack, including HTTP. |
| 67 | + |
| 68 | +### Information Files |
| 69 | + |
| 70 | +These are files like `README.md`, `CONTRIBUTING`, `CHANGELOG.txt` and so on. They contain information about the project. |
| 71 | + |
| 72 | +### Configuration Files |
| 73 | + |
| 74 | +Usually your project will utilise a couple of tools, which need configuration. Those configuration files belong to this group, e.g., `.gitignore`, `composer.json`, `codeception.yml`, `phpunit.xml`, or even `Robofile`. |
| 75 | + |
| 76 | +## Testing |
| 77 | + |
| 78 | +### Unit Tests |
| 79 | + |
| 80 | +Unit tests are tests that do not need a particular setup. They can be run in your development environment directly, without a Joomla installation. So in this context, _unit_ does not mean maximally isolated code pieces, but any testable combination. The advantage of keeping unit as small as possible is that you can locate problems more precisely, but you might have to use a lot of mocks and stubs. |
| 81 | + |
| 82 | +However, running the tests in your development environment will only cover one PHP version, the one your using on your development machine. |
| 83 | + |
| 84 | +#### Basic Solution |
| 85 | + |
| 86 | +For each PHP version you want to test with, |
| 87 | +- create a PHP container |
| 88 | +- copy your project to the container |
| 89 | +- add a script that |
| 90 | + - initialises the workspace, e.g., runs `composer install` |
| 91 | + - runs the tests |
| 92 | +- collect and merge the coverage reports. |
| 93 | + |
| 94 | +Here's a rough, unelaborated sketch of what a `Dockerfile` for a suitable image could look like: |
| 95 | + |
| 96 | +```dockerfile |
| 97 | +FROM php:${PHP_VERSION} |
| 98 | + |
| 99 | +# Install Composer and XDebug |
| 100 | +RUN curl -sS https://getcomposer.org/installer | php \ |
| 101 | + && mv composer.phar /usr/bin/composer \ |
| 102 | + && pecl channel-update pecl.php.net \ |
| 103 | + && pecl install xdebug-${XDEBUG_VERSION} \ |
| 104 | + && docker-php-ext-enable xdebug |
| 105 | + |
| 106 | +COPY . /app |
| 107 | +WORKDIR /app |
| 108 | + |
| 109 | +RUN composer install --prefer-source --no-interaction |
| 110 | + |
| 111 | +ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}" |
| 112 | + |
| 113 | +CMD ['run-sript.sh'] |
| 114 | +``` |
| 115 | + |
| 116 | +> A predefined image prepared with Composer and XDebug could be provided. |
| 117 | +
|
| 118 | +After running the container, it should not be removed automatically (i.e., don't use the `--rm` option), since the coverage data needs to be extracted. The test runner should have been configured to leave its artifacts in `/app/build`, so the results can be pulled with something like |
| 119 | + |
| 120 | +```bash |
| 121 | +$ docker cp test_container_1:/app/build build/unit_1 |
| 122 | +``` |
| 123 | + |
| 124 | +Then the container can be removed. |
| 125 | + |
| 126 | +```bash |
| 127 | +$ docker rm test_container_1 |
| 128 | +``` |
| 129 | + |
| 130 | +#### Parallelisation |
| 131 | + |
| 132 | +One single command should be used to setup, execute, and cleanup after the parallel tests. |
| 133 | + |
| 134 | +##### How it works |
| 135 | + |
| 136 | +A command container (see Appendix) is created with the following setup: |
| 137 | + |
| 138 | +- A _Sequencer_ puts the execution commands for each test class into a _CommandQueue_. The configuration should provide information about which test framework to use (usually one of PHPUnit or Codeception) for which subset of tests. |
| 139 | +- The _CommandQueue_ provides each test environment with each command. |
| 140 | +- For each test environment, a _Dispatcher_ receives the commands and delegates them to _Executor_ instances, one by one. If there are more commands than runners, the next free runner receives the next command. |
| 141 | +- An _Executor_ receives one command at a time and sends its results (f.x. coverage reports) to a _Reporter_. |
| 142 | +- The _Reporter_ merges the artifacts, and stores the consolidated reports in the `build` directory. |
| 143 | + |
| 144 | +#### Interfaces |
| 145 | + |
| 146 | +**Sequencer** |
| 147 | + |
| 148 | +```php |
| 149 | +namespace Joomla\Virtualisation\Test; |
| 150 | + |
| 151 | +interface SequencerInterface |
| 152 | +{ |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +**Command** |
| 157 | + |
| 158 | +```php |
| 159 | +namespace Joomla\Virtualisation\Test; |
| 160 | + |
| 161 | +interface CommandInterface |
| 162 | +{ |
| 163 | + public function getRunner(): string; |
| 164 | + public function getTest(): string; |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +**CommandQueue** |
| 169 | + |
| 170 | +```php |
| 171 | +namespace Joomla\Virtualisation\Test; |
| 172 | + |
| 173 | +interface CommandQueueInterface |
| 174 | +{ |
| 175 | + public function enqueue(Command $command): void; |
| 176 | + public function registerEnvironment(Dispatcher $environment): void |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +**Dispatcher** |
| 181 | + |
| 182 | +```php |
| 183 | +namespace Joomla\Virtualisation\Test; |
| 184 | + |
| 185 | +interface DispatcherInterface |
| 186 | +{ |
| 187 | + public function dispatch(Command $command): void; |
| 188 | + public function registerExecutor(Executor $executor): void |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +**Executor** |
| 193 | + |
| 194 | +```php |
| 195 | +namespace Joomla\Virtualisation\Test; |
| 196 | + |
| 197 | +use SebastianBergmann\CodeCoverage\CodeCoverage; |
| 198 | + |
| 199 | +interface ExecutorInterface |
| 200 | +{ |
| 201 | + public function run(Command $command): CodeCoverage; |
| 202 | + public function getLog(): string; |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +**Reporter** |
| 207 | + |
| 208 | +```php |
| 209 | +namespace Joomla\Virtualisation\Test; |
| 210 | + |
| 211 | +use SebastianBergmann\CodeCoverage\CodeCoverage; |
| 212 | + |
| 213 | +interface ReporterInterface |
| 214 | +{ |
| 215 | + public function merge(CodeCoverage $coverage): void; |
| 216 | + public function getXml(): string; |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +The simplest way to establish communication between the _Executor_ and the container instances is a minimal REST API. |
| 221 | + |
| 222 | +**GET /phpunit/unit/:test** |
| 223 | +- URL Parameters: |
| 224 | + - `test`: The path to the test file as needed by the runner relative to `tests/unit` |
| 225 | +- Response: |
| 226 | + - `200/OK` on success, with serialised CodeCoverage in the body |
| 227 | + - `500/Internal Server Error` on failure, with execution log in the body |
| 228 | + |
| 229 | +**GET /codecept/unit/:test** |
| 230 | +- URL Parameters: |
| 231 | + - `test`: The path to the test file as needed by the runner relative to `tests/unit` |
| 232 | +- Response: |
| 233 | + - `200/OK` on success, with serialised CodeCoverage in the body |
| 234 | + - `500/Internal Server Error` on failure, with execution log in the body |
| 235 | + |
| 236 | +# Appendix |
| 237 | + |
| 238 | +## Use a Docker Container as a Command |
| 239 | + |
| 240 | +Example: `composer` |
| 241 | + |
| 242 | +Define the following function in your `~/.bashrc`, `~/.zshrc` or similar. |
| 243 | +You can then run `composer` as if it was installed on your host locally. |
| 244 | + |
| 245 | +```bash |
| 246 | +composer () { |
| 247 | + tty= |
| 248 | + tty -s && tty=--tty |
| 249 | + docker run \ |
| 250 | + $tty \ |
| 251 | + --interactive \ |
| 252 | + --rm \ |
| 253 | + --user $(id -u):$(id -g) \ |
| 254 | + --volume /etc/passwd:/etc/passwd:ro \ |
| 255 | + --volume /etc/group:/etc/group:ro \ |
| 256 | + --volume $(pwd):/app \ |
| 257 | + composer "$@" |
| 258 | +} |
| 259 | +``` |
| 260 | + |
| 261 | +## Copy to Images, Optimise Images |
| 262 | + |
| 263 | +[dkrcp](https://github.com/WhisperingChaos/dkrcp) - Copy files between host's file system, containers, and images. |
| 264 | +It supplements `docker cp` by: |
| 265 | + |
| 266 | +- Facilitating image creation or adaptation by simply copying files. When copying to an existing image, its state is unaffected, as copy preserves its immutability by creating a new layer. |
| 267 | +- Enabling the specification of multiple copy sources, including other images, to improve operational alignment with Linux cp -a and minimize layer creation when TARGET refers to an image. |
| 268 | +- Supporting the direct expression of copy semantics where SOURCE and TARGET arguments concurrently refer to containers. |
| 269 | + |
0 commit comments