Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 85 additions & 31 deletions .claude/skills/moreunit-build/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,129 @@ description: Build and test the MoreUnit-Eclipse project (Eclipse RCP / Tycho /

# MoreUnit-Eclipse Build & Test

This project is an **Eclipse RCP / OSGi** application built with **Eclipse Tycho** (not a standard Maven project). The build is **not** a simple `mvn clean install` from the repo root — Tycho resolves a full Eclipse target platform and the reactor is declared in a nested build module.
This project is an **Eclipse RCP / OSGi** application built with **Eclipse Tycho** (not a standard Maven project). Tycho resolves a full Eclipse target platform; the reactor is declared in a nested build module.

## Where to run from

The Maven reactor lives in the **`org.moreunit.build`** module (a child of the repo root). All build commands must be launched **from that directory**, not the repo root:
The Maven reactor lives in the **`org.moreunit.build`** module. All build commands must be launched **from that directory**:

```bash
cd org.moreunit.build
```

The parent POM (`org.moreunit.build/pom.xml`) aggregates every module (`org.moreunit.core`, `org.moreunit.plugin`, `org.moreunit.core.test`, `org.moreunit.test`, `org.moreunit.swtbot.test`, features, update site, …).

## Build commands

### Full build + all tests (offline, recommended first try)
### Full build + all tests (offline)

```bash
cd org.moreunit.build && mvn -o verify -fae
```

- `-o` / **offline**: use only the local Maven/Tycho cache. The first ever run needs network to fetch the Eclipse target platform; afterwards prefer offline to avoid slow re-resolution.
- **`verify`** (not `install`): runs compilation **and** the Tycho surefire tests. Plain `mvn clean` or `compile` alone would skip the tests.
- **`-fae`** (fail-at-end): keep going through all modules even if one fails, so you see the full picture instead of stopping at the first broken module.
- `-o`: offline mode (use local Maven/Tycho cache only). First-ever run needs network for the target platform.
- `verify`: runs everything including tests.
- `-fae` (fail-at-end): continues past broken modules.

### Build a single module and its dependencies
### Build single module + dependencies

Tycho doesn't accept `-pl` the same way as classic Maven. To scope roughly, build from `org.moreunit.build` and resume, or just run the full reactor (it's the reliable path). If a module fails and you want to re-run from it:
Tycho **does** support `-pl` with standard Maven syntax:

```bash
mvn -rf :<artifactId> verify -fae
# Build core tests only (fastest signal for core changes)
mvn -o -pl ../org.moreunit.core.test -am verify -Dtests.use.ui=false -fae

# Build plugin + mock unit tests (need a display / X server)
DISPLAY=:0 mvn -o -pl ../org.moreunit.test,../org.moreunit.mock.test -am verify -fae

# Build SWTBot test (needs a display)
DISPLAY=:0 mvn -o -pl ../org.moreunit.swtbot.test -am verify -fae
```

### Skip the UI/SWTBot tests (fastest feedback on core logic)
`-am` = also make (builds upstream dependencies). `-rf :<artifactId>` resumes from a specific module.

The headless environment often flaks on SWTBot (`WidgetNotFound`, menu-bar timeouts). To get fast signal on the pure-Java core:
### Run a specific test class

```bash
mvn -o verify -fae -Dskip.swtbot.tests=true
mvn -o -pl ../org.moreunit.swtbot.test -am verify -Dtest=RunTestSWTBotTest -fae
```

(Check the parent POM properties for the exact skip property name in use; if none, just run the full reactor and ignore SWTBot noise.)
The `-Dtest=` filter applies Tycho-surefire's `test` parameter. Upstream modules with no matching class gracefully skip (0 tests).

## UI / headless execution matrix

| Module | Needs display? | Needs Workbench? | Default UI harness | How to run |
|---|---|---|---|---|
| `org.moreunit.core.test` | No (pure logic) | No | `tests.use.ui=true` | `-Dtests.use.ui=false` for headless (UI tests fail, pure tests pass) |
| `org.moreunit.test` | **Yes** | **Yes** (plugin activator calls PlatformUI) | `tests.use.ui=true` | `DISPLAY=:0 mvn ...` |
| `org.moreunit.mock.test` | **Yes** | **Yes** (transitive via mock → plugin) | `tests.use.ui=true` | `DISPLAY=:0 mvn ...` |
| `org.moreunit.swtbot.test` | **Yes** | **Yes** (SWTBot workbench) | `useUIHarness=true` | `DISPLAY=:0 mvn ...` |

## Reading the results
### Pre-existing UI-test failures (ignore when validating non-UI changes)

Tycho surefire writes per-module reports that are the **source of truth** — Maven's own log is terse (especially with `-q`). After a run, inspect:
- **headless run** (`-Dtests.use.ui=false`): `JumpActionHandlerTest`, `JumpActionExecutorTest` — need a Workbench.
- **SWTBot run** (headless or flaky): `BestMatchJumpTest`, `PropertiesTest` — timing/menu issues.
- **UI unit run**: `JumperExtensionManagerTest`, `LanguageExtensionManagerTest` — may fail intermittently in some environments.

## Coverage (JaCoCo)

### Run coverage and generate aggregate report

```bash
DISPLAY=:0 mvn -o -Pcoverage verify -fae -Dmaven.test.failure.ignore=true
```
<module>/target/surefire-reports/<TestClass>.txt # human summary
<module>/target/surefire-reports/TEST-<TestClass>.xml # machine details

This runs ALL tests (including SWTBot), produces per-module `jacoco.exec`, merges them, and generates an **aggregate HTML + CSV + XML report** at:

```
org.moreunit.build/target/site/jacoco-aggregate/
```

Example for the core matching tests:
Key files:
- `jacoco.csv` — easy to parse (columns: GROUP, PACKAGE, CLASS, INSTRUCTION_MISSED, INSTRUCTION_COVERED, ...)
- `jacoco.xml` — detailed XML (method-level data)
- `index.html` — browsable HTML report

### Find completely untested classes (instruction covered = 0) in main bundles

```bash
cat org.moreunit.core.test/target/surefire-reports/org.moreunit.core.matching.SearchEngineTest.txt
cd org.moreunit.build/target/site/jacoco-aggregate
awk -F, 'NR>1 && ($1 ~ /\/org\.moreunit\.core$/ || $1 ~ /\/org\.moreunit\.mock$/ || $1 == "org.moreunit.report/org.moreunit") && $5==0 {print $4"\t"$2"."$3}' jacoco.csv | sort -rn | head -40
```

A passing test set reads `Tests run: N, Failures: 0, Errors: 0, Skipped: 0`.
The GROUP column in jacoco.csv has the format `org.moreunit.report/<bundle-symbolic-name>` (e.g. `org.moreunit.report/org.moreunit.core`).

### Coverage setup details

## Known flaky / environment failures (do not chase these)
- JaCoCo `prepare-agent` execution (in the `coverage` profile) correctly sets `tycho.testArgLine` (not plain `argLine`) for Tycho compatibility — agent is injected into the test JVM.
- The `merge-all` execution (aggregator, `inherited=false`) collects `**/target/jacoco.exec` from all child modules.
- `org.moreunit.report` module runs `report-aggregate` to produce the unified report covering all bundles.
- Use `-Dmaven.test.failure.ignore=true` to ensure all modules package even if tests fail, so the report module's dependencies resolve.

## Module structure

```
org.moreunit.build/ ← build aggregator (pom.xml)
org.moreunit.core/ ← core logic (matching, resources, preferences, config)
org.moreunit.core.test/ ← tests for core (JUnit + Mockito)
org.moreunit.plugin/ ← Eclipse UI plugin (handlers, actions, refactoring, etc.)
org.moreunit.test/ ← unit tests for plugin (JUnit + Mockito)
org.moreunit.mock/ ← mock generation support
org.moreunit.mock.test/ ← tests for mock (fragment, can access package-private host classes)
org.moreunit.test.dependencies/ ← shared test infrastructure (TestContextRule, @Project, configs, test doubles)
org.moreunit.swtbot.test/ ← UI tests (SWTBot, needs display)
org.moreunit.report/ ← JaCoCo aggregate reporting
```

In a headless/offline run, these commonly fail **unrelated to your change** — they are SWTBot/UI timing issues:
- `org.moreunit.swtbot.test` — `BestMatchJumpTest`, `PropertiesTest` (`WidgetNotFound ... Could not find menu bar for shell`, `Could not find node with text: test`).
## Key dependencies for test modules

When verifying a fix, **focus on the surefire report of the module you touched**, not the SWTBot suite. Confirm your target module is green; treat SWTBot failures as pre-existing environment noise unless your change touched UI code.
- All test modules: `junit-jupiter-api`, `org.mockito.mockito-core`, `org.mockito.junit-jupiter`
- `org.moreunit.core.test`: `org.eclipse.ui`, `org.eclipse.core.resources` (needed for test doubles)
- `org.moreunit.test`: `org.eclipse.jdt.core`, `org.moreunit.test.dependencies`
- `org.moreunit.mock.test`: fragment of `org.moreunit.mock` (access to package-private host classes)
- `org.moreunit.swtbot.test`: `org.eclipse.swtbot.go` / `swtbot.junit5_x`

## Sanity check workflow
### Access restrictions (common gotchas)

1. `cd org.moreunit.build`
2. `mvn -o verify -fae`
3. Grep the surefire reports of the module you care for `Failures: 0, Errors: 0`.
4. If green there → your change is verified, regardless of SWTBot noise elsewhere.
Test bundles may not import packages that the production bundles use. This causes `Access restriction` compile errors:
- `org.eclipse.jface.text.Position` — restricted in `org.moreunit.test` (org.eclipse.text bundle). **Fix:** avoid referencing `Position` type in tests, or add the package to Require-Bundle (test infra change).
- `org.eclipse.debug.core.DebugPlugin` — restricted in `org.moreunit.swtbot.test`. **Fix:** add `org.eclipse.debug.core` to swtbot.test MANIFEST Require-Bundle.
- `org.eclipse.debug.core.ILaunch` — same restriction as DebugPlugin.
224 changes: 224 additions & 0 deletions .claude/skills/moreunit-test-patterns/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
---
name: moreunit-test-patterns
description: How to write and locate tests in the MoreUnit-Eclipse project. Covers unit test structure, SWTBot UI tests, test doubles, JaCoCo coverage analysis, and common patterns. Use when the user asks to write tests, find what's untested, increase coverage, or understand the test infrastructure.
---

# MoreUnit-Eclipse Test Patterns

## Test module layout

Each main module has a corresponding test module. Test source folders vary:

| Module | Test source folders | Notes |
|---|---|---|
| `org.moreunit.core.test` | `test/` (tests) + `src/` (test infra: InMemoryWorkspace, etc.) + `classes/` (fixture classes) | Fragment of `org.moreunit.core` |
| `org.moreunit.test` | `test/` (all tests) | Fragment of `org.moreunit` (plugin) |
| `org.moreunit.mock.test` | `test/` (all tests) | Fragment of `org.moreunit.mock` |
| `org.moreunit.swtbot.test` | `test/` (all SWTBot tests) | Needs display |

## Test framework & libraries

All test modules use:
- **JUnit 5** (`org.junit.jupiter.api`, `org.junit.jupiter.engine`)
- **Mockito** (`org.mockito.mockito-core`, `org.mockito.junit-jupiter`)
- Tests use static imports: `assert*` from `org.junit.jupiter.api.Assertions`, `mock`/`when`/`verify` from `org.mockito.Mockito`

### Naming convention

Test classes follow `{ClassName}Test.java`. Test method names follow `should_<description>` or `should_<action>_<condition>` style.

## Unit test patterns (core.test)

### Test doubles for resource classes

The `org.moreunit.core.resources` package has a complete **in-memory implementation** used as test doubles:

| Double | Implements | Purpose |
|---|---|---|
| `InMemoryWorkspace` | `Workspace`, `ResourceContainer` | Full workspace with projects, folders, files |
| `InMemoryProject` | `Project`, `InMemoryResourceContainer` | Java-like project |
| `InMemoryFolder` | `Folder`, `InMemoryResourceContainer` | Folder within a project |
| `InMemoryFile` | `File`, `InMemoryResource` | File with content |
| `InMemoryPath` | `Path` (`Iterable<String>`) | Path parsing and manipulation |
| `InMemoryResource` | `Resource` | Base resource |
| `InMemoryResourceContainer` | `ResourceContainer` | Container for children |

These are in `org.moreunit.core.test/src/org/moreunit/core/resources/`. Usage example:

```java
InMemoryWorkspace workspace = new InMemoryWorkspace();
Path path = workspace.path("/proj/src/foo");
InMemoryFolder folder = workspace.getFolder("/proj/src");
```

### Mocking Eclipse interfaces

For "pure logic + mockable Eclipse interfaces" pattern (most common for core/modifable classes):

```java
// Mock an interface like IFile, IResource, IPreferenceStore, IMethod, etc.
IFile file = mock(IFile.class);
when(file.toString()).thenReturn("path/to/file");

// Verify interactions
verify(file).delete(true, null);
verify(parent, never()).delete(anyBoolean(), any());
```

### Mocking via InOrder

For verifying call order (e.g., `ContainerCreation`):

```java
InOrder order = inOrder(parent, container);
order.verify(parent).create();
order.verify(container).create();
```

## SWTBot test patterns

### Base class

All SWTBot tests extend `JavaProjectSWTBotTestHelper` (in `org.moreunit.swtbot.test/test/org/moreunit/`), which provides:
- `bot` (static `SWTWorkbenchBot`) — the SWTBot API entry point
- `context` (`@RegisterExtension TestContextRule`) — manages the workspace
- `openResource("SomeClass.java")` — opens a file in the editor via "Navigate > Open Resource..."
- `getShortcutStrategy()` — returns platform-specific shortcut strategy
- `testSimpleJump(original, target)` — common jump test helper
- `selectAndReturnJavaProjectFromPackageExplorer()` — selects project in Package Explorer
- Automatic editor cleanup between tests (`@BeforeEach`/`@AfterEach` closes all editors)

### Project setup via annotations

SWTBot tests use the `@Context` or `@Project` annotations to define the workspace.

**Using a pre-configured project class:**
```java
@ExtendWith(SWTBotJunit5Extension.class)
public class MyTest extends JavaProjectSWTBotTestHelper {
@Test
@Context(SimpleJUnit4Project.class)
public void my_test() {
openResource("SomeClass.java");
getShortcutStrategy().pressJumpShortcut();
// ...
}
}
```

**Inline project definition:**
```java
@Test
@Project(
mainCls = "org:SomeClass",
testCls = "org:SomeClassTest",
properties = @Properties(
testType = TestType.JUNIT4,
testClassNameTemplate = "${srcFile}Test"))
public void my_test() { ... }
```

**From source files (resources):**
```java
@Test
@Project(
mainSrc = "MyClass_with_method.txt",
testSrc = "MyClassTest_with_testmethod.txt",
properties = @Properties(...))
public void my_test() { ... }
```

The `.txt` files are resolved as resources relative to the test class's package.

### Available `@Context` project configs

All in `org.moreunit.test.dependencies/src/org/moreunit/test/context/configs/`:

| Config class | Test type | Source/test classes |
|---|---|---|
| `SimpleJUnit3Project` | JUnit 3 | SomeClass / SomeClassTest |
| `SimpleJUnit4Project` | JUnit 4 | SomeClass / SomeClassTest |
| `SimpleJUnit5Project` | JUnit 5 | SomeClass / SomeClassTest |
| `SimpleTestNGProject` | TestNG | SomeClass / SomeClassTest |
| `SimpleSpockProject` | Spock | SomeClass / SomeClassSpec |
| `SimpleMavenJUnit4Project` | JUnit 4 + Maven structure | SomeClass / SomeClassTest |

### Key keyboard shortcuts

Defined in `ShortcutStrategy` and `plugin.xml`:

| Action | Shortcut | Method |
|---|---|---|
| Jump to Test / Test Subject | `Ctrl+Shift+J` | `pressJumpShortcut()` |
| Generate Test / Create Test | `Ctrl+U` | `pressGenerateShortcut()` |
| Run Test | `Ctrl+R` | (via `KeyboardFactory.getSWTKeyboard().pressShortcut(SWT.CTRL, 'r')`) |
| Debug Test | `Ctrl+Alt+D` | (not in helper) |

### Launch-based assertion (Run/Debug test)

To assert a test was actually launched, check the LaunchManager directly. Requires `org.eclipse.debug.core` in `Require-Bundle` of `swtbot.test/META-INF/MANIFEST.MF`:

```java
int launchesBefore = DebugPlugin.getDefault().getLaunchManager().getLaunches().length;
KeyboardFactory.getSWTKeyboard().pressShortcut(SWT.CTRL, 'r');
bot.waitUntil(new DefaultCondition() {
@Override
public boolean test() {
return DebugPlugin.getDefault().getLaunchManager().getLaunches().length > launchesBefore;
}
// ...
});
```

## Coverage analysis (finding what's untested)

### Quick: parse the JaCoCo CSV

After a coverage run with `-Pcoverage`:

```bash
cd org.moreunit.build/target/site/jacoco-aggregate

# Completely untested classes (instr covered = 0), sorted by impact
awk -F, 'NR>1 && ($1 ~ /\/org\.moreunit\.core$/ || $1 ~ /\/org\.moreunit\.mock$/ || $1 == "org.moreunit.report/org.moreunit") && $5==0 {print $4"\t"$2"."$3}' jacoco.csv | sort -rn

# Classes with any coverage, sorted by % covered (ascending, worst first)
awk -F, 'NR>1 && ($1 ~ /\/org\.moreunit\.core$/ || $1 ~ /\/org\.moreunit\.mock$/ || $1 == "org.moreunit.report/org.moreunit") && ($4+$5)>0 {pct=$5/($4+$5)*100; printf "%5.1f%%\t%s.%s\n", pct, $2, $3}' jacoco.csv | sort -n
```

The GROUP column format is `org.moreunit.report/<bundle-symbolic-name>`.

Main bundles (not tests): `org.moreunit.report/org.moreunit.core`, `org.moreunit.report/org.moreunit`, `org.moreunit.report/org.moreunit.mock`.

### Verify a specific test file is genuinely new

Before writing a new test, check the file does NOT already exist on master:

```bash
git cat-file -e HEAD:"org.moreunit.core.test/test/org/moreunit/core/my/MyClassTest.java" 2>/dev/null && echo "EXISTS" || echo "NEW"
```

Or check via `git ls-files`:

```bash
git ls-files '*/test/*MyClassTest.java'
```

## Access restrictions (when your test won't compile)

Test bundles may not import packages used by the production code. Common restrictions:

| Restricted API | Bundle | In which test module | Fix |
|---|---|---|---|
| `org.eclipse.jface.text.Position` (type + fields) | `org.eclipse.text` | `org.moreunit.test` | Avoid referencing Position; or add `org.eclipse.text` to Require-Bundle |
| `org.eclipse.debug.core.DebugPlugin` | `org.eclipse.debug.core` | `org.moreunit.swtbot.test` | Add `org.eclipse.debug.core` to Require-Bundle |
| `org.eclipse.debug.core.ILaunch` / `ILaunchManager` | `org.eclipse.debug.core` | `org.moreunit.swtbot.test` | Same as above |

## Adding a new dependency to a test bundle

Edit the `Require-Bundle` entry in the test module's `META-INF/MANIFEST.MF`. Add on a new line (comma-terminated):

```
Require-Bundle: ...,
org.eclipse.debug.core,
```
Loading
Loading