Skip to content

Commit e7b7232

Browse files
committed
Started extension-dev.md (WIP)
1 parent 3b49134 commit e7b7232

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

docs/extension-dev.md

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

Comments
 (0)