This is an automated email from the ASF dual-hosted git repository.
Fokko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git
The following commit(s) were added to refs/heads/master by this push:
new f420b6425 Add AGENTS.md with AssertJ/JUnit5 test migration guidelines
(#3728)
f420b6425 is described below
commit f420b64250781dca5a2b1ca56d105695106fc78a
Author: Eduard Tudenhoefner <[email protected]>
AuthorDate: Mon Sep 14 14:17:38 2026 +0200
Add AGENTS.md with AssertJ/JUnit5 test migration guidelines (#3728)
* Add AGENTS.md with AssertJ/JUnit5 test migration guidelines
* move into separate files
---
AGENTS.md | 32 ++
agents/docs/migrating-tests.md | 278 ++++++++++++++++
agents/docs/writing-tests.md | 697 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 1007 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..1c6841a0a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,32 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Agent guidelines
+
+Reference documentation for AI agents working in this repository. Read the
+linked document for the task at hand rather than loading everything up front.
+
+## Tests
+
+Tests use **JUnit 5 + [AssertJ](https://assertj.github.io/doc/)**. Before
writing,
+reviewing, or migrating tests, read the relevant guide under
+[`agents/docs/`](./agents/docs):
+
+- Writing or reviewing tests →
[`agents/docs/writing-tests.md`](./agents/docs/writing-tests.md)
+- Migrating JUnit 4 tests →
[`agents/docs/migrating-tests.md`](./agents/docs/migrating-tests.md)
diff --git a/agents/docs/migrating-tests.md b/agents/docs/migrating-tests.md
new file mode 100644
index 000000000..22f8a1094
--- /dev/null
+++ b/agents/docs/migrating-tests.md
@@ -0,0 +1,278 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Migrating tests (JUnit 4 → JUnit 5 + AssertJ)
+
+Process for **converting existing JUnit 4 tests** to JUnit 5 + AssertJ. The
+**target style** for the migrated code is defined in
+[`writing-tests.md`](./writing-tests.md) — read that first. This document adds
+the JUnit → AssertJ mapping, guidance for a bulk first pass, and the
+post-migration audit that catches what a bulk `assertEquals` → `assertThat`
+rewrite leaves behind.
+
+## JUnit → AssertJ mapping
+
+| JUnit | AssertJ |
+|-------|---------|
+| `assertEquals(expected, actual)` | `assertThat(actual).isEqualTo(expected)` |
+| `assertSame(expected, actual)` | `assertThat(actual).isSameAs(expected)` |
+| `assertArrayEquals(expected, actual)` |
`assertThat(actual).isEqualTo(expected)` or `containsExactly(...)` for
primitives |
+| `assertEquals(n, collection.size())` | `assertThat(collection).hasSize(n)` |
+| `assertEquals(collection.size(), other.size())` |
`assertThat(collection).hasSameSizeAs(other)` |
+| `assertTrue(collection.contains(x))` | `assertThat(collection).contains(x)` |
+| `assertFalse(collection.contains(x))` |
`assertThat(collection).doesNotContain(x)` |
+| `assertTrue(collection.isEmpty())` | `assertThat(collection).isEmpty()` |
+| `assertFalse(collection.isEmpty())` | `assertThat(collection).isNotEmpty()` |
+| `assertEquals(0, command.run())` | `assertThat(command.run()).isZero()` |
+| `assertEquals(0, file.length())` | `assertThat(file.length()).isZero()` |
+| `assertTrue(0 < file.length())` | `assertThat(file.length()).isPositive()` |
+| `assertTrue(file.exists())` | `assertThat(file).exists()` |
+| `@Test(expected = X.class)` / `assertThrows(X.class, …)` |
`assertThatThrownBy(…).isInstanceOf(X.class).hasMessage(…)` |
+| `assertTrue(condition)` | `assertThat(condition).isTrue()` |
+| `assertNull(x)` | `assertThat(x).isNull()` |
+| `assertTrue(true)` after successful run | `assertThatCode(() ->
action()).doesNotThrowAnyException()` |
+| `assert (exitCode == 0)` | `assertThat(exitCode).isZero()` |
+| `Assume.assumeTrue(condition)` (JUnit 4) | `assumeThat(condition).isTrue()`
(AssertJ) |
+| `Assumptions.assumeTrue(condition)` (JUnit 5) |
`assumeThat(condition).isTrue()` (AssertJ) |
+| `assertEquals(Collections.emptyList(), list)` | `assertThat(list).isEmpty()`
|
+| `assertEquals(List.of(a, b, …), list)` |
`assertThat(list).containsExactly(a, b, …)` |
+| `assertEquals(otherList, list)` (ordered) |
`assertThat(list).containsExactlyElementsOf(otherList)` |
+| `TestUtils.assertThrows(…)` |
`assertThatThrownBy(…).isInstanceOf(…).hasMessage(…)` |
+| try/catch + `fail` for expected exception | `assertThatThrownBy(…)` or
`assertThatCode(…).doesNotThrowAnyException()` |
+
+See [`writing-tests.md`](./writing-tests.md) for the full rationale behind each
+target form (actual-vs-expected order, collection/optional/iterator assertions,
+`@TempDir`, assumptions, and exception style).
+
+## Migrating custom `assertThrows` helpers
+
+Many legacy tests used helpers like `assertThrows(String description, Class<?
extends Exception> expected, Runnable r)` that only verified the **exception
class**. The first `String` parameter was a **test description** (shown when no
exception was thrown), **not** `exception.getMessage()`.
+
+When converting these helpers:
+
+1. Replace with `assertThatThrownBy` + `isInstanceOf`.
+2. **Also** add `hasMessage` / `hasMessageContaining` using the **actual**
message from the production `throw` statement or a test run — do **not** copy
the old description string into `hasMessage`.
+
+```java
+// Legacy helper — description is NOT the exception message
+assertThrows(
+ "Should not allow TIME_MICROS with " + primitive,
+ IllegalArgumentException.class,
+ () -> new AvroSchemaConverter().convert(message(type)));
+
+// Wrong migration — old description used as message
+assertThatThrownBy(() -> new AvroSchemaConverter().convert(message(type)))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Should not allow TIME_MICROS with " + primitive);
+
+// Correct migration — message from the throw site
+assertThatThrownBy(() -> new AvroSchemaConverter().convert(message(type)))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("…"); // actual text from throw new
IllegalArgumentException(…)
+```
+
+## Bulk first pass
+
+Start with a **syntax-only** bulk rewrite (search-and-replace or a scripted
pass) that converts a subset of JUnit `Assert` calls and swaps imports. This is
a mechanical first pass only — it does **not** produce finished AssertJ style
on its own.
+
+### What a bulk rewrite can safely do
+
+| Input | Output |
+|-------|--------|
+| `assertEquals(expected, actual)` | `assertThat(actual).isEqualTo(expected)` |
+| `assertArrayEquals(expected, actual)` |
`assertThat(actual).isEqualTo(expected)` |
+| `assertNull` / `assertNotNull` | `isNull` / `isNotNull` |
+| `assertTrue` / `assertFalse` | `isTrue` / `isFalse` on the **same boolean
expression** |
+| `assertSame` / `assertNotSame` | `isSameAs` / `isNotSameAs` |
+| `Assert.fail(…)` | AssertJ `fail(…)` |
+| JUnit `Assert` / Hamcrest imports | AssertJ static imports (when Assert was
present) |
+
+### What the bulk pass does **not** handle (requires a manual second pass)
+
+Everything in [`writing-tests.md`](./writing-tests.md) that is **not** in the
table above — including but not limited to:
+
+| Category | Left behind by script | Target |
+|----------|----------------------|--------|
+| **Exceptions** | `catchThrowable` + separate `assertThat(caught)` |
`assertThatThrownBy` + `isInstanceOf` + `hasMessage*` |
+| **Exceptions** | `@Test(expected)`, JUnit `assertThrows`,
`TestUtils.assertThrows`, try/catch/`fail` | `assertThatThrownBy` +
`isInstanceOf` + `hasMessage*` |
+| **Exceptions** | `assertThatThrownBy` with only `isInstanceOf` | Add
`hasMessage` / `hasMessageContaining` from the `throw` site |
+| **Exceptions** | Old `assertThrows(description, …)` description copied into
`hasMessage` | Real exception text, not the test description |
+| **Collections** | `isEqualTo(Collections.emptyList())`,
`isEqualTo(List.of(…))`, `isEqualTo(otherList)` | `isEmpty()`,
`containsExactly(…)`, `containsExactlyElementsOf(…)` |
+| **Collections** | `assertThat(x.contains(…)).isTrue()` /
`isEmpty().isTrue()` / `size().isEqualTo(n)` | `contains`, `isEmpty`,
`hasSize`, `hasSameSizeAs`, … |
+| **Optional** | `assertThat(opt.isPresent()).isTrue()` / `isFalse()` |
`assertThat(opt).isPresent()` / `isEmpty()` |
+| **Numeric** | `assertThat(comparison).isTrue()` / `isFalse()` | `isZero`,
`isPositive`, `isGreaterThan`, `hasSizeLessThan`, … |
+| **Numeric** | `assertThat(x).isEqualTo(0)` on integral results |
`assertThat(x).isZero()` |
+| **Files** | `assertThat(file.exists()).isTrue()` |
`assertThat(file).exists()` |
+| **Strings** | `assertThat(x.toString())` | `assertThat(x).asString()` |
+| **Assumptions** | `Assume.assumeTrue` / `Assumptions.assumeTrue` /
`org.junit.jupiter.api.Assumptions` | AssertJ `assumeThat` |
+| **Temp dirs** | `@Rule TemporaryFolder`, package-private `@TempDir` |
`@TempDir private Path tempDir` (+ accessor for subclasses) |
+| **Temp paths** | `toFile().getAbsolutePath()`, `new File(tempDir.toFile(),
…)`, create+delete before writer | `tempDir.resolve(…)` + `new Path(….toUri())`
|
+| **Ordering** | `assertThat(a.compareTo(b) < 0).isTrue()` |
`assertThat(a).isLessThan(b)` / `isEqualByComparingTo` |
+| **Ordering** | `assertThat(compare(a, b)).isPositive()` etc. | Keep, but
migrate from `assertTrue(compare(…) > 0)` |
+| **Type / reference** | `instanceof` / `==` / `\|\|` wrapped in `isTrue()` |
`isInstanceOf`, `isSameAs`, `isIn` |
+| **Iterators** | Custom drain helpers (`assertIteratorEquals`, …) |
`assertThat(it).toIterable().containsExactly(…)` |
+| **Iterators** | `assertThat(it.hasNext()).isFalse()` /
`assertFalse(it.hasNext())` | `assertThat(it).isExhausted()` |
+| **Streams** | `assertThat(result).isEqualTo(stream.collect(…))` |
`containsExactlyElementsOf(…)` after collecting |
+| **Maps / sets** | `hasSize` + `containsKey` / `contains` on same subject |
`containsOnlyKeys` / `containsExactlyInAnyOrder` |
+| **No exception** | `assertTrue(true)`, empty try/catch |
`assertThatCode(…).doesNotThrowAnyException()` |
+| **Style** | `.as("…")` on `assertThatThrownBy` | Omit |
+| **Style** | Block lambda with one statement in `assertThatThrownBy` |
Expression lambda or method reference |
+| **Bugs** | `assertThat(expr.method()).isEqualTo(…)` — AssertJ chained on the
**value** | `assertThat(expr).method(…)` — chain on the **assertion** |
+
+**A bulk rewrite is not sufficient.** After any bulk `assertEquals` →
`assertThat` edit, always run the post-migration audit below before considering
a module done.
+
+## Post-migration audit
+
+Run these checks on the migrated tree. Review every hit — some patterns are
heuristics and may have false positives (e.g. `isEqualTo(0)` on a non-numeric
field).
+
+Replace `<module-test>` with the module's `src/test` directory (e.g.
`parquet-hadoop/src/test`).
+
+### JUnit / Hamcrest leftovers
+
+```bash
+rg 'import static org\.junit\.Assert|import
org\.junit\.Assert|org\.hamcrest|org\.junit\.Assume|Assumptions\.assumeTrue'
<module-test>
+```
+
+### Exceptions
+
+```bash
+# Not yet migrated to AssertJ
+rg '@Test\(expected|TestUtils\.assertThrows|\bassertThrows\(' <module-test>
+rg 'catchThrowable\(' <module-test>
+
+# Type-only exception checks (missing message assertion)
+rg 'assertThatThrownBy\([^;]+\)\s*\.isInstanceOf\([^)]+\)\s*;' <module-test>
+
+# Invalid or discouraged
+rg 'assertThatThrownBy\([^)]+\)\s*\.as\(' <module-test>
+rg 'hasMessage\(\(String\) null\)|hasMessage\(null\)' <module-test>
+
+# try/catch used only to assert an exception (manual review)
+rg -U 'catch\s*\([^)]+\)\s*\{[^}]*fail\(' <module-test>
+```
+
+When converting `TestUtils.assertThrows` or JUnit `assertThrows`, add
`hasMessage` from the production `throw` statement — **not** from the old test
description string.
+
+### Collections and sizes
+
+```bash
+rg
'isEqualTo\(Collections\.emptyList\(\)\)|isEqualTo\(List\.of\(|isEqualTo\(java\.util\.List\.of\('
<module-test>
+rg '\.contains\([^)]+\)\.(isTrue|isFalse)\(\)' <module-test>
+rg '\.isEmpty\(\)\.(isTrue|isFalse)\(\)' <module-test>
+rg 'assertThat\([^)]+\.size\(\)\)\.' <module-test>
+rg '\.hasSize\([^)]+\.(length|size)\(\)\)' <module-test>
+rg 'assertIteratorEquals|assertAllRowsEqual' <module-test>
+rg 'assertThat\([^)]+\.isPresent\(\)\)\.(isTrue|isFalse)' <module-test>
+```
+
+### Iterators and streams
+
+```bash
+rg 'assertThat\([^)]+\.hasNext\(\)\)\.(isTrue|isFalse)' <module-test>
+rg 'assertFalse\([^)]+\.hasNext\(\)\)' <module-test>
+rg 'assertThat\([^)]+\)\.isEqualTo\([^)]*\.stream\(\)' <module-test>
+rg 'assertThat\([^)]+\.length\)\.isEqualTo' <module-test>
+```
+
+### Numeric and boolean comparisons
+
+```bash
+rg 'assertThat\([^)]*[<>!=]=?[^)]*\)\.(isTrue|isFalse)\(\)' <module-test>
+rg 'assertThat\([^)]+\)\.isEqualTo\(0\)|assertThat\([^)]+\)\.isEqualTo\(0L\)'
<module-test>
+```
+
+Manually fix hits such as `assertThat(file.length()).isEqualTo(0)` →
`isZero()`, `assertThat(x).isPositive()`, etc.
+
+### Files, strings, assumptions
+
+```bash
+rg '\.exists\(\)\)\.(isTrue|isFalse)|assertTrue\([^)]*\.exists\(\)'
<module-test>
+rg 'assertThat\([^)]+\.toString\(\)\)' <module-test>
+rg
'Assume\.assumeTrue|Assumptions\.assumeTrue|org\.junit\.jupiter\.api\.Assumptions'
<module-test>
+rg -U '@TempDir\s*\n\s*(public |protected )?[A-Za-z]' <module-test>
+rg 'TemporaryFolder|@Rule.*[Tt]emp' <module-test>
+rg 'tempDir\.toFile\(\)|new
File\(tempDir\.toFile\(\)|toFile\(\)\.getAbsolutePath\(\)' <module-test>
+rg 'Files\.createFile\(tempDir' <module-test>
+rg 'AvroDirectWriterTest' <module-test>
+```
+
+### Ordering, type, and reference equality
+
+```bash
+rg 'compareTo\([^)]+\)\s*(<|>|==|!=)' <module-test>
+rg 'assertThat\([^)]*instanceof[^)]*\)\.(isTrue|isFalse)' <module-test>
+rg 'assertThat\([^)]*==[^)]*\)\.isTrue' <module-test>
+rg 'assertThat\([^)]*\|\|[^)]*\)\.isTrue' <module-test>
+```
+
+### Broken or flipped assertion chains
+
+```bash
+# AssertJ API wrongly nested inside assertThat(...) — often a compile error or
silent wrong subject
+rg
'assertThat\([^)]+\.(isEqualTo|isNull|isNotNull|isTrue|isFalse|contains|hasSize)\('
<module-test>
+
+# Literal or constant as subject (heuristic — review each hit)
+rg 'assertThat\((true|false|null|\d+|"[^"]*")\)' <module-test>
+```
+
+### No exception expected
+
+```bash
+rg 'assertTrue\(true\)|assertThat\(true\)\.isTrue' <module-test>
+rg '\bassert\s+' <module-test>
+```
+
+## Suggested workflow
+
+1. Do a bulk first pass on `<module-test>` (search-and-replace or scripted),
or migrate by hand.
+2. Fix compile errors (wrong generics on `assertThatThrownBy`, broken nesting,
etc.).
+3. Run **all** audit `rg` commands above; fix every real violation.
+4. `./mvnw spotless:apply -pl <module>`
+5. `mvn -pl <module> test` (or `test-compile` if full test run is blocked).
+
+A module is not migration-complete until the audit is clean (aside from
documented exceptions) **and** tests pass.
+
+## Partial migration
+
+When only exception tests are migrated in a file that otherwise stays on JUnit
4:
+
+- Add AssertJ imports only for `assertThatThrownBy` / `assertThatCode`
+- Leave existing `assertEquals` / `assertTrue` unchanged unless explicitly
migrating the whole file
+
+When the **whole file or module** is being migrated, ignore the “leave
unchanged” rule — apply all conventions in
[`writing-tests.md`](./writing-tests.md) and run the post-migration audit.
+
+## Verification
+
+After changes, apply formatting, run the post-migration audit, and run tests
for the affected module:
+
+```bash
+./mvnw spotless:apply -pl <module>
+mvn -pl <module> test
+```
+
+Example:
+
+```bash
+# bulk first pass on parquet-hadoop/src/test (search-and-replace or by hand)
+# … run audit rg commands from Post-migration audit …
+./mvnw spotless:apply -pl parquet-hadoop
+mvn -pl parquet-hadoop test
+```
+
+Run `spotless:apply` before committing so import order and formatting match
the project style.
diff --git a/agents/docs/writing-tests.md b/agents/docs/writing-tests.md
new file mode 100644
index 000000000..05d0ec4ca
--- /dev/null
+++ b/agents/docs/writing-tests.md
@@ -0,0 +1,697 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# Writing tests (JUnit 5 + AssertJ)
+
+Conventions for **writing new tests** and **reviewing new/changed tests** in
this
+repository using [AssertJ](https://assertj.github.io/doc/). Use JUnit 5
+(`org.junit.jupiter.api.*`) for test lifecycle and AssertJ for all assertions,
+exceptions, and assumptions.
+
+> Migrating an existing JUnit 4 test to this style? Start with
+> [`migrating-tests.md`](./migrating-tests.md) — it covers the mechanical first
+> pass, the JUnit → AssertJ mapping table, and the post-migration audit. The
+> conventions below define the **target style** those migrations aim for.
+
+## Imports
+
+- Use `import static org.assertj.core.api.Assertions.assertThat;`
+- Use `import static org.assertj.core.api.Assertions.assertThatThrownBy;` for
exception tests
+- Use `import static org.assertj.core.api.Assertions.assertThatCode;` when
verifying no exception is thrown
+- Use `import static org.assertj.core.api.Assumptions.assumeThat;` for
conditional test execution (skip when assumption fails) — **not** JUnit
`Assume`, JUnit 5 `Assumptions`, or any other assumption API
+- **Do not** import from `AssertionsForClassTypes` unless there is a genuine
generic ambiguity (e.g. `invoke()` return values). Prefer `Assertions` first.
+- Use JUnit 5 (`org.junit.jupiter.api.*`) for test lifecycle only — `@Test`,
`@BeforeEach`, `@AfterEach`, `@TempDir`, etc. Use AssertJ for assertions,
exceptions, and assumptions.
+
+## `@TempDir`
+
+Use JUnit 5 `@TempDir` instead of JUnit 4 `TemporaryFolder` / `@Rule`:
+
+```java
+import org.junit.jupiter.api.io.TempDir;
+import java.nio.file.Path;
+
+@TempDir
+private Path tempDir;
+```
+
+- Declare `@TempDir` fields **`private`** by default — not package-private or
`public`.
+- Prefer `java.nio.file.Path` on the test class; expose a `protected` accessor
(e.g. `getTempFolder()`) when subclasses need the directory.
+- When both `java.nio.file.Path` and `org.apache.hadoop.fs.Path` are in scope,
declare `@TempDir` as `private java.nio.file.Path tempDir` and **do not**
import `java.nio.file.Path` if it would clash with Hadoop `Path`.
+
+```java
+// Wrong — package-private or public
+@TempDir
+Path tempDir;
+
+// Wrong — JUnit 4
+@Rule
+public TemporaryFolder temp = new TemporaryFolder();
+
+// Correct
+@TempDir
+private Path tempDir;
+
+// Correct when Hadoop Path is also used
+@TempDir
+private java.nio.file.Path tempDir;
+```
+
+### Path handling (`@TempDir` → Hadoop `Path` / `File`)
+
+Stay on `java.nio.file.Path` for temp-dir work. Convert to Hadoop or
`java.io.File` only at API boundaries.
+
+**Hadoop `Path` from `@TempDir`:**
+
+```java
+// Correct — child path for a writer/reader
+Path output = new Path(tempDir.resolve("out.parquet").toUri());
+
+// Correct — unique file that must not exist yet (CREATE mode)
+Path output = new Path(tempDir.resolve(UUID.randomUUID().toString()).toUri());
+
+// Correct — directory root
+Path root = new Path(tempDir.toUri());
+```
+
+Prefer `.toUri()` when constructing Hadoop `Path` from `java.nio.file.Path` —
do not round-trip through `toFile().getAbsolutePath()` or `toString()` on an
intermediate variable unless the API requires a `String`.
+
+**`java.io.File` when required:**
+
+```java
+File file = tempDir.resolve("child.parquet").toFile();
+```
+
+Use `tempDir.resolve("name")`, not `new File(tempDir.toFile(), "name")`.
+
+**Do not pre-create files for Hadoop CREATE-mode writers.** Parquet/Hadoop
writers open with create-if-not-exists semantics and fail with
`FileAlreadyExistsException` when the path already exists. A UUID (or other
unique name) under `@TempDir` is enough — no `Files.createFile` /
`temp.newFile()` + delete dance.
+
+```java
+// Wrong — creates then deletes, or leaves a file that breaks CREATE
+File temp = Files.createTempFile(tempDir, "test", ".tmp").toFile();
+Path path = new Path(temp.getAbsolutePath());
+
+java.nio.file.Path tempFile =
Files.createFile(tempDir.resolve(UUID.randomUUID().toString()));
+Files.delete(tempFile);
+Path path = new Path(tempFile.toString());
+
+// Wrong — redundant conversion
+new Path(tempDir.toFile().getAbsolutePath())
+new Path(new File(tempDir.toFile(), "out").getAbsolutePath())
+
+// Correct — path does not exist yet; writer creates it
+Path path = new Path(tempDir.resolve(UUID.randomUUID().toString()).toUri());
+```
+
+**`Files.createTempFile` / `Files.createTempDirectory`:** keep only when the
test needs JDK temp-name generation inside `@TempDir` (e.g. a specific
prefix/suffix). If the created path is passed to a CREATE-mode writer, delete
it first with `Files.delete(path)`. Prefer `tempDir.resolve(...)` or
`Files.createTempDirectory(tempDir, "prefix")` + `new Path(dir.toUri())` over
`.toFile().getAbsolutePath()`.
+
+**Shared test bases:** use `org.apache.parquet.DirectWriterTest` from the
`parquet-hadoop` test-jar for direct `RecordConsumer` writing — do not
duplicate it in other modules (e.g. no module-local `AvroDirectWriterTest`).
+
+```java
+import org.apache.parquet.DirectWriterTest;
+
+public class TestArrayCompatibility extends DirectWriterTest {
+```
+
+## Actual vs expected order
+
+AssertJ uses **actual first, expected second**:
+
+```java
+// Correct
+assertThat(stream.read()).isEqualTo(124);
+assertThat(column("a").compareTo(column("b"))).isEqualTo(-1);
+assertThat(cw.getEncoding()).isEqualTo(PLAIN_DICTIONARY);
+
+// Wrong (flipped)
+assertThat(124).isEqualTo(stream.read());
+assertThat(PLAIN_DICTIONARY).isEqualTo(cw.getEncoding());
+```
+
+## Assumptions
+
+Use AssertJ `assumeThat` (`import static
org.assertj.core.api.Assumptions.assumeThat`) to skip tests when a precondition
is not met (OS-specific tests, optional features, etc.). Do **not** use JUnit 4
`Assume`, JUnit 5 `org.junit.jupiter.api.Assumptions`, or any JUnit assumption
helper.
+
+Prefer fluent assertions on the value under test over wrapping a boolean
expression in `.isTrue()`:
+
+```java
+// Wrong — JUnit 4
+Assume.assumeTrue(System.getProperty("os.name").toLowerCase().startsWith("win"));
+
+// Wrong — JUnit 5
+Assumptions.assumeTrue(System.getProperty("os.name").toLowerCase().startsWith("win"));
+
+// Wrong — JUnit 5 static import of Assumptions (still JUnit, not AssertJ)
+import static org.junit.jupiter.api.Assumptions.*;
+assumeTrue(featureEnabled);
+
+// Correct — AssertJ assumeThat
+assumeThat(featureEnabled).isTrue();
+
+// Correct — fluent check on the subject
+assumeThat(System.getProperty("os.name").toLowerCase()).startsWith("win");
+```
+
+When an assumption fails, AssertJ throws `org.opentest4j.TestAbortedException`
(same as JUnit 5), so the test is reported as skipped.
+
+## Exception assertions
+
+Use `assertThatThrownBy` for expected exceptions:
+
+```java
+assertThatThrownBy(() -> action())
+ .isInstanceOf(SomeException.class)
+ .hasMessage("exact message");
+```
+
+Every `assertThatThrownBy` chain **must** include `.isInstanceOf(...)` **and**
a message assertion (`.hasMessage`, `.hasMessageContaining`, or
`.hasMessageStartingWith`) unless the thrown exception reliably has a `null`
message (see below).
+
+**Always** use `assertThatThrownBy` — not `catchThrowable` followed by
`assertThat(caught)`:
+
+```java
+// Wrong — split capture and assertion; does not fail clearly when no
exception is thrown
+Throwable caught = catchThrowable(() -> action());
+assertThat(caught).isInstanceOf(BadConfigurationException.class);
+assertThat(caught).hasMessageContaining("23");
+
+// Correct
+assertThatThrownBy(() -> action())
+ .isInstanceOf(BadConfigurationException.class)
+ .hasMessageContaining("23");
+```
+
+Use `.satisfies(…)`, `.hasSuppressedException(…)`,
`.hasNoSuppressedExceptions()`, or `.isSameAs(…)` on the `assertThatThrownBy`
chain when additional checks on the thrown instance are needed. Reserve
`catchThrowable` only when the test must **continue after** capturing (e.g.
multiple independent actions in one test) — not for a single expected failure.
+
+`isInstanceOf` alone is **not** sufficient — it loses coverage of the error
text users and operators actually see.
+
+### Message checks
+
+- **Always** add `.hasMessage(...)`, `.hasMessageContaining(...)`, or
`.hasMessageStartingWith(...)` when the exception has a non-null message.
+- Use `.hasMessage(...)` when the full message is stable and known (copy it
from the `throw` site or a failing test).
+- Use `.hasMessageContaining(...)` when the message includes variable detail
(type names, field paths, etc.) or when several loop iterations share a common
substring.
+- **Omit** `hasMessage` only when the JDK exception message is reliably `null`
(e.g. `EOFException`, `InvalidMarkException`, `ReadOnlyBufferException`,
re-thrown checked exceptions with no message).
+- **Never** use `.hasMessage(null)` or `.hasMessage((String) null)`.
+
+```java
+// Wrong — type only
+assertThatThrownBy(() -> new
AvroSchemaConverter().convert(parquetSchemaWithInt96))
+ .isInstanceOf(IllegalArgumentException.class);
+
+// Correct
+assertThatThrownBy(() -> new
AvroSchemaConverter().convert(parquetSchemaWithInt96))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "INT96 is deprecated. As interim enable READ_INT96_AS_FIXED flag to
read as byte array.");
+
+// Correct — shared substring across similar failures
+assertThatThrownBy(() -> new AvroSchemaConverter().convert(message(type)))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Cannot annotate schema");
+```
+
+### `assertThatThrownBy` style
+
+- **Do not** put `.as("description")` on `assertThatThrownBy` chains. Rely on
the test method name or inline the intent in the lambda.
+- **Do** keep `.as(DECIMAL)`, `.as(logicalType)`, etc. on **builder** chains
inside the lambda — those are not AssertJ descriptions.
+- Use an **expression lambda** for a single action. Reserve block lambdas (`()
-> { … }`) for multiple statements.
+
+```java
+// Wrong — unnecessary block for one call
+assertThatThrownBy(() -> {
+ evaluate(neverCalled);
+ })
+ .isInstanceOf(ShortCircuitException.class)
+ .hasMessage("…");
+
+// Correct — expression lambda (or method reference when it fits)
+assertThatThrownBy(() -> evaluate(neverCalled))
+ .isInstanceOf(ShortCircuitException.class)
+ .hasMessage("…");
+assertThatThrownBy(command::run)
+ .isInstanceOf(FileAlreadyExistsException.class)
+ .hasMessageContaining("File already exists");
+
+// Block lambda is fine when setup is required
+assertThatThrownBy(() -> {
+ MessageType incompatible = new MessageType("schema", …);
+ readGroups(store, originalSchema, incompatible, 1);
+ })
+ .isInstanceOf(ParquetDecodingException.class)
+ .hasMessage("…");
+```
+
+```java
+// Wrong
+assertThatThrownBy(() -> builder.build())
+ .as("Should reject invalid type")
+ .isInstanceOf(IllegalArgumentException.class);
+
+// Correct
+assertThatThrownBy(() -> builder.build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("…");
+
+// Builder .as() inside lambda is fine
+assertThatThrownBy(() -> Types.required(INT32).as(DECIMAL).named("x"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("…");
+```
+
+### No exception expected
+
+Replace no-op `assertTrue(true)` after a successful call,
try/catch/`Assert.fail` blocks, or Java `assert` statements with:
+
+```java
+assertThatCode(() ->
uuidConverter.addBinary(binary)).doesNotThrowAnyException();
+assertThatCode(() -> ToolRunner.run(conf, new Main(logger),
args)).doesNotThrowAnyException();
+```
+
+## Numeric results
+
+Prefer dedicated numeric assertions over boolean comparisons:
+
+```java
+// Wrong
+assertThat(command.run()).isEqualTo(0);
+assertThat(0 < file.length()).isTrue();
+assertThat(columnSizeInBytes.get("DocId") >
columnSizeInBytes.get("Num")).isTrue();
+
+// Correct
+assertThat(command.run()).isZero();
+assertThat(file.length()).isPositive();
+assertThat(columnSizeInBytes.get("DocId")).isGreaterThan(columnSizeInBytes.get("Num"));
+```
+
+Use `.isZero()`, `.isPositive()`, `.isNegative()`, `.isGreaterThan(...)`, etc.
on the **value under test**.
+
+## Files
+
+```java
+// Wrong
+assertThat(output.exists()).isTrue();
+assertThat(outputFile.length()).isEqualTo(0);
+
+// Correct
+assertThat(output).exists();
+assertThat(outputFile.length()).isZero();
+assertThat(avroFile.length()).isPositive();
+```
+
+## String / `toString()` comparisons
+
+Use AssertJ's `.asString()` on the **subject**, not `.toString()` in
`assertThat(...)`:
+
+```java
+// Wrong
+assertThat(schema.toString()).isEqualTo(expectedMT.toString());
+assertThat(stats.toString()).isEqualTo("min: 1.0, max: 2.0, num_nulls: 0");
+
+// Correct — object compared to object (use .toString() on expected to avoid
format overload)
+assertThat(schema).asString().isEqualTo(expectedMT.toString());
+
+// Correct — object compared to string literal
+assertThat(stats).asString().isEqualTo("min: 1.0, max: 2.0, num_nulls: 0");
+assertThat(pred).asString().isEqualTo("or(and(not(…");
+assertThat(schemaString).contains("\"name\" : \"timestamp_1\"");
+assertThat(schemaString).doesNotContain("\"type\" : [ \"null\", \"INT96\" ]");
+
+// Correct — builder chain
+assertThat(Types.required(FIXED_LEN_BYTE_ARRAY).length(16).as(uuidType()).named("uuid_field"))
+ .asString()
+ .isEqualTo("required fixed_len_byte_array(16) uuid_field (UUID)");
+```
+
+**Important:** `asString().isEqualTo(otherObject)` can hit AssertJ's
`isEqualTo(String format, Object...)` overload. When comparing to another
object, use `.isEqualTo(other.toString())` on the expected side.
+
+`.as("description")` **before** `.asString()` is allowed on normal assertions:
+
+```java
+assertThat(records.get(0))
+ .as("deserialization does not display the same result")
+ .asString()
+ .isEqualTo(r1.toString());
+```
+
+## Collections and sizes
+
+Prefer AssertJ **collection assertions** on the collection itself. Assert on
the collection (`list`, `conversions`, `records`, `encodings`, …), not on
`size()`, `isEmpty()`, or `contains()` return values.
+
+### Size
+
+**Always** use `hasSize(n)` instead of
`assertThat(collection.size()).isEqualTo(n)` or
`assertThat(array.length).isEqualTo(n)`.
+
+When comparing two collections (or a collection and an array) that should have
the same length, prefer `hasSameSizeAs` over `hasSize(other.size())` or
`hasSize(array.length)`:
+
+```java
+// Wrong
+assertThat(splits.size()).isEqualTo(offsets.length);
+assertThat(splits).hasSize(sizes.length);
+assertThat(actual.size()).isEqualTo(expected.size());
+
+// Correct
+assertThat(splits).hasSameSizeAs(offsets);
+assertThat(splits).hasSameSizeAs(sizes);
+assertThat(actual).hasSameSizeAs(expected);
+```
+
+Use `hasSizeLessThan` / `hasSizeGreaterThan` when asserting a relative size
bound, not a boolean comparison on sizes:
+
+```java
+// Wrong
+assertThat(result.size() < DATA.size()).isTrue();
+
+// Correct
+assertThat(result).hasSizeLessThan(DATA.size());
+```
+
+### Membership, emptiness, and equality
+
+Do not wrap collection state in a boolean and assert with `isTrue()` /
`isFalse()`:
+
+```java
+// Wrong
+assertThat(column.getEncodings().contains(Encoding.PLAIN_DICTIONARY))
+ .as("Column should be dictionary encoded: " + name)
+ .isTrue();
+assertThat(column.getEncodings().contains(Encoding.PLAIN))
+ .as("Column should not have plain data pages" + name)
+ .isFalse();
+assertThat(list).isEqualTo(List.of(1, 2, 3));
+assertThat(list).isEqualTo(Collections.emptyList());
+assertThat(list.size()).isEqualTo(3);
+assertThat(list.isEmpty()).isTrue();
+assertThat(!list.isEmpty()).isFalse();
+assertThat(list.contains(item)).isTrue();
+assertThat(metadata.getBlocks().isEmpty()).isFalse();
+assertThat(metadata.getBlocks().size() > 0).isTrue();
+
+// Correct
+assertThat(column.getEncodings())
+ .as("Column should be dictionary encoded: " + name)
+ .contains(Encoding.PLAIN_DICTIONARY);
+assertThat(column.getEncodings())
+ .as("Column should not have plain data pages" + name)
+ .doesNotContain(Encoding.PLAIN);
+assertThat(list).containsExactly(1, 2, 3);
+assertThat(list).isEmpty();
+assertThat(list).isNotEmpty();
+assertThat(list).hasSize(3);
+assertThat(list).contains(item);
+assertThat(metadata.getBlocks()).isNotEmpty();
+```
+
+Use `containsExactly` for ordered list equality with literal elements. Use
`containsExactlyElementsOf` when comparing to another iterable. Use
`containsExactlyInAnyOrder` only when order does not matter (e.g.
`HashSet`-backed iterables).
+
+**Exception:** `assertThat(stream.size()).isEqualTo(n)` on `BytesInput` /
stream-like types is fine — `hasSize()` applies to collections, not arbitrary
`size()` methods.
+
+**Exception:** `Statistics.isEmpty()`, `hasNonNullValue()`, and similar domain
`boolean` methods are not Java collections — keep
`assertThat(stats.isEmpty()).isFalse()` or assert the specific stat fields
directly.
+
+## Optional (`java.util.Optional`)
+
+Assert on the **Optional**, not on `optional.isPresent()` /
`optional.isEmpty()` wrapped in `isTrue()` / `isFalse()`:
+
+```java
+// Wrong
+assertThat(page.getCrc().isPresent()).as("Checksum was not set in
page").isTrue();
+assertThat(page.getCrc().isPresent()).as("Checksum was set in page").isFalse();
+assertThat(offsetIndex.getUnencodedByteArrayDataBytes(0).isPresent()).isFalse();
+
+// Correct
+assertThat(page.getCrc()).as("Checksum was not set in page").isPresent();
+assertThat(page.getCrc()).as("Checksum was set in page").isEmpty();
+assertThat(offsetIndex.getUnencodedByteArrayDataBytes(0)).isEmpty();
+```
+
+For a present value, prefer `assertThat(opt).contains(value)` or
`assertThat(opt).get().isEqualTo(value)` over
`assertThat(opt.isPresent()).isTrue()` followed by `opt.get()`.
+
+**Exception:** `Statistics.isEmpty()` and other domain `boolean` methods are
not `Optional` — see Collections above.
+
+## Streams and iterators
+
+### Iterator content
+
+Replace custom helpers that drain an iterator into a list/array and compare
with `isEqualTo` (e.g. `assertIteratorEquals`, `assertAllRowsEqual`). Use
AssertJ's `IteratorAssert.toIterable()` to consume the iterator and apply
iterable assertions:
+
+```java
+// Wrong — custom helper draining the iterator
+static void assertIteratorEquals(PrimitiveIterator.OfInt actualIt, int...
expectedValues) {
+ IntList actualList = new IntArrayList();
+ actualIt.forEachRemaining(actualList::add);
+ assertThat(actualList.toIntArray()).isEqualTo(expectedValues);
+}
+assertIteratorEquals(IndexIterator.all(10), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
+
+// Correct — fluent AssertJ on the iterator
+assertThat(IndexIterator.all(10)).toIterable().containsExactly(0, 1, 2, 3, 4,
5, 6, 7, 8, 9);
+assertThat(IndexIterator.intersection(lhs, rhs)).toIterable().isEmpty();
+assertThat(ranges.iterator()).toIterable().containsExactly(1L, 2L, 3L, 4L);
+```
+
+Notes:
+
+- `PrimitiveIterator.OfInt` / `OfLong` extend `Iterator`, so
`assertThat(iterator)` works directly.
+- `.toIterable()` **consumes** the iterator (same as a drain helper).
Non-consuming checks use `hasNext()` / `isExhausted()` on the iterator itself
(see below).
+- For `PrimitiveIterator.OfLong`, `containsExactly` expects `Long` varargs —
use `L` suffixes on literals (`1L`, `2L`, …).
+- For `int...` expected values passed as a varargs parameter, box before
spreading into `containsExactly`:
+
+```java
+assertThat(predicate.accept(ci))
+ .toIterable()
+
.containsExactly(Arrays.stream(expectedIndexes).boxed().toArray(Integer[]::new));
+```
+
+Do **not** add a custom AssertJ assertion class unless the same pattern is
needed across many modules — `toIterable()` is sufficient.
+
+### Iterator state (`hasNext` / `isExhausted`)
+
+Assert on the **iterator**, not on `iterator.hasNext()` wrapped in `isTrue()`
/ `isFalse()`:
+
+```java
+// Wrong
+assertThat(expIt.hasNext()).isFalse();
+assertThat(expIt.hasNext()).isTrue();
+
+// Correct
+assertThat(expIt).isExhausted();
+assertThat(expIt).hasNext();
+```
+
+Use `.isExhausted()` when all elements were consumed (e.g. after a partial
match loop). Use `.hasNext()` when more elements are expected. `.as("…")`
before these is allowed when the failure message needs context.
+
+### `java.util.stream.Stream`
+
+AssertJ has no `StreamAssert` — **collect first**, then assert on the
resulting collection:
+
+```java
+// Wrong — boolean wrapper on stream terminal operation
+assertThat(result.size() == expected.size()).isTrue();
+
+// Wrong — isEqualTo on two independently collected lists (often fine, but
prefer iterable API)
+assertThat(result).isEqualTo(DATA.stream().filter(pred).collect(Collectors.toList()));
+
+// Correct — collect actual, then compare as iterable
+List<User> result = readFilteredUsers(filter);
+assertThat(result).containsExactlyElementsOf(DATA.stream().filter(expectedFilter).collect(Collectors.toList()));
+
+// Correct — known expected elements
+assertThat(result).containsExactly(userA, userB);
+```
+
+For a `Stream` used only as an expected source in a helper, passing
`stream.iterator()` to `assertThat(it).toIterable()` is fine when the stream is
consumed once.
+
+When asserting a **Java array** length, prefer `assertThat(array).hasSize(n)`
over `assertThat(array.length).isEqualTo(n)`.
+
+### Maps and sets — combine related checks
+
+Prefer one assertion when size and membership are asserted together:
+
+```java
+// Wrong — three assertions for the same map
+assertThat(map).hasSize(2);
+assertThat(map).containsKey(keyA);
+assertThat(map).containsKey(keyB);
+
+// Correct
+assertThat(map).containsOnlyKeys(keyA, keyB);
+
+// Wrong — size + contains on a set
+assertThat(set).hasSize(2);
+assertThat(set).contains("hello", "world");
+
+// Correct
+assertThat(set).containsExactlyInAnyOrder("hello", "world");
+```
+
+Use `containsOnlyKeys` when the map must have **exactly** those keys. Use
`containsExactlyInAnyOrder` for sets (order irrelevant). Use `containsExactly`
when order matters.
+
+### When `isTrue()` / `isFalse()` is still fine
+
+Use `isTrue()` / `isFalse()` for genuine boolean expressions that are not
collection, string, **type**, **reference equality**, or **membership** checks,
e.g. `filter.keep(path)` or method calls that return `boolean` without a
dedicated AssertJ assertion.
+
+Do **not** use `assertThat(x instanceof Foo.class).isTrue()` — use
`assertThat(x).isInstanceOf(Foo.class)` instead. For negation, use
`isNotInstanceOf(...)`.
+
+Do **not** use `assertThat(a == b).isTrue()` for reference equality — use
`assertThat(a).isSameAs(b)` instead.
+
+Do **not** use `assertThat(x == a || x == b).isTrue()` for “one of these
values” — use `assertThat(x).isIn(a, b)` instead (or `isNotIn(...)` for
negation).
+
+```java
+// Wrong
+assertThat(records.get(0).get("dec") instanceof BigDecimal).isTrue();
+assertThat(obj instanceof Map).as("Should be a map").isTrue();
+assertThat(car.getDoors() == 4 || car.getDoors() == 5).isTrue();
+
+// Correct
+assertThat(records.get(0).get("dec")).isInstanceOf(BigDecimal.class);
+assertThat(obj).as("Should be a map").isInstanceOf(Map.class);
+assertThat(car.getDoors()).isIn(4, 5);
+```
+
+## Ordering and comparison
+
+### `Comparable` / `compareTo`
+
+```java
+// Wrong
+assertThat(a.compareTo(b) < 0).isTrue();
+assertThat(a.compareTo(b) == 0).isTrue();
+
+// Correct
+assertThat(a).isLessThan(b);
+assertThat(a).isGreaterThan(b);
+assertThat(a).isEqualByComparingTo(b);
+```
+
+### `compare()` returning `int`
+
+When the API returns `int` (not `Comparable` on the subject), assert on the
result:
+
+```java
+assertThat(Float16.compare(a, b)).isZero();
+assertThat(Float16.compare(a, b)).isPositive();
+assertThat(Float16.compare(a, b)).isNegative();
+```
+
+For custom `Comparator` instances comparing values directly:
+
+```java
+assertThat(truncated)
+ .usingComparator(comparator)
+ .isLessThanOrEqualTo(value);
+```
+
+Do **not** use `usingComparator` with `Float16::compare` on boxed `Short`
values — it does not match `Float16.compare(short, short)` semantics.
+
+### Reference equality (`isSameAs`)
+
+The **value under test** is the AssertJ subject; the known reference is the
argument:
+
+```java
+// Correct — slice buffer is under test
+assertThat(one.array()).as("Should use the same backing
array").isSameAs(data.array());
+
+// Correct — dictionary-encoded field should reuse the same object reference
+assertThat(car.getModel()).isSameAs(previousCar.getModel());
+
+// Wrong — backing array as subject
+assertThat(data.array()).isSameAs(one.array());
+
+// Wrong — reference equality via isTrue()
+assertThat(car.model == previousCar.model).isTrue();
+```
+
+`.as("…")` on non-exception assertions is allowed.
+
+## Arrays and bytes
+
+```java
+// Prefer
+assertThat(bytes).containsExactly(1, 2, 3);
+assertThat(bytes).hasSize(12);
+assertThat(buffer).isEqualTo(expectedArray);
+
+// Byte values from streams — cast to avoid JUnit-style promotion issues
+assertThat((int) stream.read()).isEqualTo(i);
+assertThat((byte) buffer.get()).isEqualTo((byte) i);
+```
+
+## Booleans and filters
+
+Replace custom match helpers with direct AssertJ:
+
+```java
+// Wrong
+assertMatches(filter, path);
+assertDoesNotMatch(filter, path);
+
+// Correct
+assertThat(filter.keep(path)).isTrue();
+assertThat(filter.keep(path)).isFalse();
+```
+
+## Patterns to avoid (quick reference)
+
+| Avoid | Use instead |
+|-------|-------------|
+| `catchThrowable` + `assertThat(caught).isInstanceOf` / `hasMessage*` |
`assertThatThrownBy(…).isInstanceOf(…).hasMessage*` |
+| `assertThatThrownBy` without `hasMessage` | Add `hasMessage` /
`hasMessageContaining` from the `throw` site |
+| `@Rule TemporaryFolder` / `temp.newFile()` | `@TempDir private Path tempDir`
+ `tempDir.resolve(...)` |
+| `tempDir.toFile().getAbsolutePath()` / `new File(tempDir.toFile(), …)` |
`tempDir.resolve(…).toUri()` or `.toFile()` |
+| `new Path(tempDir.toFile().getAbsolutePath())` | `new
Path(tempDir.resolve(…).toUri())` |
+| `Files.createFile` + `Files.delete` before CREATE-mode writer |
`tempDir.resolve(uniqueName)` only — file must not exist |
+| Duplicate `DirectWriterTest` in other modules | `extends DirectWriterTest`
from `parquet-hadoop` test-jar |
+| `Assume.assumeTrue(...)` / `Assumptions.assumeTrue(...)` | AssertJ
`assumeThat(...).isTrue()` or fluent `assumeThat(value).…` |
+| `singleElement()` | `containsExactly(element)` or explicit checks |
+| `.satisfies(…)` | Direct assertions unless truly necessary |
+| `assertThat(x.toString())` | `assertThat(x).asString()` |
+| `assertThat(list.isEmpty()).isTrue()` | `assertThat(list).isEmpty()` |
+| `assertThat(list.isEmpty()).isFalse()` /
`assertThat(!list.isEmpty()).isTrue()` | `assertThat(list).isNotEmpty()` |
+| `assertThat(opt.isPresent()).isTrue()` | `assertThat(opt).isPresent()` |
+| `assertThat(opt.isPresent()).isFalse()` /
`assertThat(opt.isEmpty()).isTrue()` | `assertThat(opt).isEmpty()` |
+| `assertThat(list.contains(x)).isTrue()` | `assertThat(list).contains(x)` |
+| `assertThat(list.contains(x)).isFalse()` |
`assertThat(list).doesNotContain(x)` |
+| `assertThat(list).isEqualTo(otherList)` |
`assertThat(list).containsExactlyElementsOf(otherList)` |
+| `assertThat(list.size()).isEqualTo(n)` | `assertThat(list).hasSize(n)` |
+| `assertThat(list).hasSize(other.size())` / `hasSize(array.length)` |
`assertThat(list).hasSameSizeAs(other)` |
+| `assertThat(a.size() < b.size()).isTrue()` |
`assertThat(a).hasSizeLessThan(b)` |
+| `assertThat(array.length).isEqualTo(n)` | `assertThat(array).hasSize(n)` |
+| `assertThat(it).toIterable()` for drained iterators |
`assertThat(it).toIterable().containsExactly(1, 2, 3)` |
+| `assertThat(it.hasNext()).isFalse()` | `assertThat(it).isExhausted()` |
+| `assertThat(it.hasNext()).isTrue()` | `assertThat(it).hasNext()` |
+| `assertThat(list).isEqualTo(stream.collect(toList()))` |
`assertThat(list).containsExactlyElementsOf(stream.collect(toList()))` |
+| `hasSize(n)` + `containsKey` / `contains` on same map or set |
`containsOnlyKeys(…)` / `containsExactlyInAnyOrder(…)` |
+| `assertThat(x instanceof Foo).isTrue()` |
`assertThat(x).isInstanceOf(Foo.class)` |
+| `assertThat(a == b).isTrue()` | `assertThat(a).isSameAs(b)` |
+| `assertThat(x == a \|\| x == b).isTrue()` | `assertThat(x).isIn(a, b)` |
+| `assertThat(x).isEqualTo(0)` on `int`/`long` results |
`assertThat(x).isZero()` |
+| `assertThat(0 < x).isTrue()` | `assertThat(x).isPositive()` |
+| `assertThat(file.exists()).isTrue()` | `assertThat(file).exists()` |
+| `assertThat(true).isTrue()` / try-catch for no exception |
`assertThatCode(() -> …).doesNotThrowAnyException()` |
+| `.as()` on `assertThatThrownBy` | (omit) |
+| `assertThatThrownBy(() -> { action(); })` (single statement) |
`assertThatThrownBy(() -> action())` or method reference |
+| `hasMessage(null)` | (omit `hasMessage`) |
+| Constant as assertion subject | Expression under test as subject |
+| `hasCauseInstanceOf` when exception is thrown directly | Use `isInstanceOf`
on the thrown type |
+
+## Verification
+
+After writing or changing tests, apply formatting and run tests for the
affected module:
+
+```bash
+./mvnw spotless:apply -pl <module>
+mvn -pl <module> test
+```