Skip to content

Commit f623116

Browse files
committed
feat(it): sync easypdf-it to 1.0.x (skip webmvc IT on javax)
1 parent a153f1a commit f623116

11 files changed

Lines changed: 478 additions & 0 deletions

File tree

easypdf-it/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# easypdf-it — End-to-end Integration Tests
2+
3+
集成测试模块。`mvn verify` 时由 `maven-failsafe-plugin``**/*IT.java`,与单元测试
4+
`maven-surefire-plugin``**/*Test.java`)解耦。
5+
6+
## 当前状态
7+
8+
- **Activity**(无 fixture 依赖,每个 `mvn verify` 都跑):
9+
- `MarkdownContractIT#missingFileSurfacesAsNotFound`
10+
- `WebMvcPdfViewIT#abstractViewRendersValidPdf`(3.0.x 专属,jakarta servlet)
11+
- **Disabled**@Disabled,等待 fixture / 模板 / 字体就位):
12+
- `MarkdownContractIT#singlePagePlainText`
13+
- `MarkdownContractIT#multiPageWithTable`
14+
- `MarkdownContractIT#taggedRoundTrip`
15+
- `MarkdownContractIT#cjkFallback`
16+
- `MarkdownContractIT#extractWithReportPopulatesMetrics`
17+
- `MarkdownContractIT#encryptedPdfIsClassified`
18+
- `TierCancellationIT#cancellationHaltsExtraction`
19+
- `LimitsIT#maxFileBytesTripsBeforeRead`
20+
- `LimitsIT#maxPagesTripsOnOpen`
21+
- `HtmlTemplateIT#*`
22+
- `CjkFontIT#*`
23+
24+
## 启用某个 Disabled 用例的步骤
25+
26+
1.`src/test/resources/contracts/` 提交对应 fixture PDF(确定性的,可用 iText 单测生成)
27+
2.`src/test/resources/snapshots/` 提交人工 review 过的 markdown 期望输出
28+
3. 移除对应测试方法上的 `@Disabled` 注解
29+
4. 在对应分支上跑 `mvn -pl easypdf-it verify` 确认通过
30+
31+
## 跨分支状态
32+
33+
| 分支 | 集成测试范围 |
34+
|---|---|
35+
| `feature/3.0.x` | 完整(contract + render + webmvc,jakarta servlet) |
36+
| `feature/2.0.x` | 同步 contract + render(javax servlet,跳过 webmvc) |
37+
| `feature/1.0.x` | 同步 contract + render(javax servlet,跳过 webmvc) |
38+
39+
`WebMvcPdfViewIT` 仅在 3.0.x 维护——2.0.x / 1.0.x 用的是 `javax.servlet` API,单独维护
40+
两个分支的 fork 成本太高。后续如有 webmvc IT 需求,改为按分支 switch import。

easypdf-it/pom.xml

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<parent>
9+
<groupId>io.github.easy4j</groupId>
10+
<artifactId>easypdf</artifactId>
11+
<version>1.0.x.20260630-SNAPSHOT</version>
12+
</parent>
13+
14+
<artifactId>easypdf-it</artifactId>
15+
<description>End-to-end integration tests: PDF→Markdown contract, html2pdf render,
16+
Spring MVC view layer. Runs via maven-failsafe-plugin (not surefire).</description>
17+
<name>${project.groupId}:${project.artifactId}</name>
18+
<packaging>jar</packaging>
19+
20+
<properties>
21+
<!-- IT 类不会作为主 jar 的一部分分发;保留 jar 包装便于 mvn install -DskipITs -->
22+
<maven.install.skip>${maven.test.skip}</maven.install.skip>
23+
</properties>
24+
25+
<dependencies>
26+
<!-- 主 API 面(PDF→Markdown / DocumentChunker / ExtractReport) -->
27+
<dependency>
28+
<groupId>${project.groupId}</groupId>
29+
<artifactId>easypdf-xhtml</artifactId>
30+
<version>${project.version}</version>
31+
</dependency>
32+
33+
<!-- 模板引擎 → PDF 渲染 -->
34+
<dependency>
35+
<groupId>${project.groupId}</groupId>
36+
<artifactId>easypdf-beetl</artifactId>
37+
<version>${project.version}</version>
38+
</dependency>
39+
<dependency>
40+
<groupId>${project.groupId}</groupId>
41+
<artifactId>easypdf-freemarker</artifactId>
42+
<version>${project.version}</version>
43+
</dependency>
44+
<dependency>
45+
<groupId>${project.groupId}</groupId>
46+
<artifactId>easypdf-thymeleaf</artifactId>
47+
<version>${project.version}</version>
48+
</dependency>
49+
50+
<!-- Spring MVC 视图层端到端 -->
51+
<dependency>
52+
<groupId>${project.groupId}</groupId>
53+
<artifactId>easypdf-webmvc</artifactId>
54+
<version>${project.version}</version>
55+
</dependency>
56+
<dependency>
57+
<groupId>${project.groupId}</groupId>
58+
<artifactId>easypdf-core</artifactId>
59+
<version>${project.version}</version>
60+
</dependency>
61+
62+
<!-- 3.0.x 上 webmvc 已迁 jakarta;webmvc 自己把 servlet-api 标 provided
63+
所以 IT 模块需要显式补上才能编译/测试 servlet 包 import。
64+
2.0.x / 1.0.x 分支删除此依赖(webmvc 用 javax.servlet-api,
65+
且 webmvc IT 已删除)。-->
66+
67+
<!-- Test runtime -->
68+
<dependency>
69+
<groupId>org.junit.jupiter</groupId>
70+
<artifactId>junit-jupiter</artifactId>
71+
</dependency>
72+
<dependency>
73+
<groupId>org.assertj</groupId>
74+
<artifactId>assertj-core</artifactId>
75+
</dependency>
76+
<dependency>
77+
<groupId>org.slf4j</groupId>
78+
<artifactId>slf4j-simple</artifactId>
79+
<scope>test</scope>
80+
</dependency>
81+
82+
<!-- Mock servlet for WebMVC IT(不引入完整 Spring 容器,跑得更快) -->
83+
<dependency>
84+
<groupId>org.springframework</groupId>
85+
<artifactId>spring-test</artifactId>
86+
</dependency>
87+
<dependency>
88+
<groupId>org.springframework</groupId>
89+
<artifactId>spring-webmvc</artifactId>
90+
</dependency>
91+
</dependencies>
92+
93+
<build>
94+
<plugins>
95+
<!-- Failsafe 跑 *IT.java,绑到 verify 阶段 -->
96+
<plugin>
97+
<groupId>org.apache.maven.plugins</groupId>
98+
<artifactId>maven-failsafe-plugin</artifactId>
99+
<version>${maven-failsafe-plugin.version}</version>
100+
<executions>
101+
<execution>
102+
<goals>
103+
<goal>integration-test</goal>
104+
<goal>verify</goal>
105+
</goals>
106+
</execution>
107+
</executions>
108+
</plugin>
109+
110+
<!-- IT 不参与 JaCoCo 90% 门(合约测试覆盖率独立) -->
111+
<plugin>
112+
<groupId>org.jacoco</groupId>
113+
<artifactId>jacoco-maven-plugin</artifactId>
114+
<configuration>
115+
<skip>true</skip>
116+
</configuration>
117+
</plugin>
118+
119+
<!-- 默认跳过主 jar 打包(IT 模块无 main 代码) -->
120+
<plugin>
121+
<groupId>org.apache.maven.plugins</groupId>
122+
<artifactId>maven-jar-plugin</artifactId>
123+
<configuration>
124+
<skipIfEmpty>true</skipIfEmpty>
125+
</configuration>
126+
</plugin>
127+
</plugins>
128+
</build>
129+
</project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.github.easy4j.pdf.it.contract;
2+
3+
import io.github.easy4j.pdf.xhtml.convert.ExtractionException;
4+
import io.github.easy4j.pdf.xhtml.convert.PdfStructureExtractor;
5+
import io.github.easy4j.pdf.xhtml.convert.layout.PdfExtractionProperties;
6+
import org.junit.jupiter.api.Disabled;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.io.File;
11+
import java.net.URL;
12+
import java.nio.file.Files;
13+
import java.nio.file.Paths;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
17+
18+
/**
19+
* Limits contract: PdfExtractionProperties.maxFileBytes / maxPages guard must trip
20+
* LIMIT_EXCEEDED before the parser opens the document.
21+
*
22+
* <p><b>Status:</b> requires real multi-page fixture PDFs of known size. Disabled
23+
* until fixtures land.
24+
*/
25+
class LimitsIT {
26+
27+
@Test
28+
@Disabled("enable after a fixture with known size is committed")
29+
@DisplayName("maxFileBytes: oversized file trips LIMIT_EXCEEDED before read")
30+
void maxFileBytesTripsBeforeRead() throws Exception {
31+
File pdf = fixture("any-valid.pdf");
32+
long actual = Files.size(Paths.get(pdf.toURI()));
33+
34+
PdfExtractionProperties tight = PdfExtractionProperties.defaults();
35+
tight.maxFileBytes = actual - 1; // 故意比实际小 1 字节
36+
37+
assertThatThrownBy(() -> PdfStructureExtractor.extract(pdf, tight))
38+
.isInstanceOf(ExtractionException.class)
39+
.extracting(e -> ((ExtractionException) e).getCode())
40+
.isEqualTo(ExtractionException.Code.LIMIT_EXCEEDED);
41+
}
42+
43+
@Test
44+
@Disabled("enable after multi-page-with-known-count.pdf fixture is committed")
45+
@DisplayName("maxPages: oversized page count trips LIMIT_EXCEEDED on open")
46+
void maxPagesTripsOnOpen() throws Exception {
47+
File pdf = fixture("multi-page-with-known-count.pdf");
48+
49+
PdfExtractionProperties tight = PdfExtractionProperties.defaults();
50+
tight.maxPages = 1;
51+
52+
assertThatThrownBy(() -> PdfStructureExtractor.extract(pdf, tight))
53+
.isInstanceOf(ExtractionException.class)
54+
.extracting(e -> ((ExtractionException) e).getCode())
55+
.isEqualTo(ExtractionException.Code.LIMIT_EXCEEDED);
56+
}
57+
58+
private static File fixture(String name) {
59+
URL url = LimitsIT.class.getResource("/contracts/" + name);
60+
assertThat((Object) url).as("fixture %s missing", name).isNotNull();
61+
return Paths.get(url.getPath()).toFile();
62+
}
63+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.github.easy4j.pdf.it.contract;
2+
3+
import io.github.easy4j.pdf.xhtml.convert.ExtractReport;
4+
import io.github.easy4j.pdf.xhtml.convert.ExtractionException;
5+
import io.github.easy4j.pdf.xhtml.convert.ExtractorMetrics;
6+
import io.github.easy4j.pdf.xhtml.convert.PdfStructureExtractor;
7+
import io.github.easy4j.pdf.xhtml.convert.layout.PdfExtractionProperties;
8+
import org.junit.jupiter.api.Disabled;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.io.File;
13+
import java.net.URL;
14+
import java.nio.file.Paths;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
18+
19+
/**
20+
* Contract suite: PDF → Markdown deterministic output for hand-picked fixture PDFs.
21+
*
22+
* <p>Fixtures live under {@code src/test/resources/contracts/} and snapshots under
23+
* {@code src/test/resources/snapshots/}. Snapshots MUST be hand-reviewed; the
24+
* purpose of this suite is to catch <em>unintended</em> drift in extractor output,
25+
* not to silently rubber-stamp it.
26+
*
27+
* <p><b>Status:</b> snapshot-driven cases are {@link Disabled @Disabled} until the
28+
* fixtures (deterministic PDFs) and expected markdown snapshots are committed by
29+
* hand. Non-fixture negative cases run on every {@code mvn verify}.
30+
*/
31+
class MarkdownContractIT {
32+
33+
private static final String SNAPSHOTS = "/snapshots";
34+
private static final String CONTRACTS = "/contracts";
35+
36+
private static File fixture(String name) {
37+
URL url = MarkdownContractIT.class.getResource(CONTRACTS + "/" + name);
38+
assertThat((Object) url).as("fixture %s missing — commit it under src/test/resources/contracts/", name)
39+
.isNotNull();
40+
return Paths.get(url.getPath()).toFile();
41+
}
42+
43+
@Test
44+
@Disabled("enable after single-page-plain.pdf fixture + snapshot are committed")
45+
@DisplayName("contract: single-page plain text")
46+
void singlePagePlainText() throws Exception {
47+
String md = PdfStructureExtractor.extract(fixture("single-page-plain.pdf"))
48+
.toMarkdown();
49+
assertThat(md).isEqualTo(readSnapshot("single-page-plain"));
50+
}
51+
52+
@Test
53+
@Disabled("enable after multi-page-table.pdf fixture + snapshot are committed")
54+
@DisplayName("contract: multi-page with table")
55+
void multiPageWithTable() throws Exception {
56+
String md = PdfStructureExtractor.extract(fixture("multi-page-table.pdf"))
57+
.toMarkdown();
58+
assertThat(md).isEqualTo(readSnapshot("multi-page-table"));
59+
}
60+
61+
@Test
62+
@Disabled("enable after tagged-report.pdf fixture + snapshot are committed")
63+
@DisplayName("contract: tagged PDF round-trip")
64+
void taggedRoundTrip() throws Exception {
65+
String md = PdfStructureExtractor.extract(fixture("tagged-report.pdf"))
66+
.toMarkdown();
67+
assertThat(md).isEqualTo(readSnapshot("tagged-report"));
68+
}
69+
70+
@Test
71+
@Disabled("enable after cjk-mixed.pdf fixture + snapshot are committed")
72+
@DisplayName("contract: cjk font fallback")
73+
void cjkFallback() throws Exception {
74+
String md = PdfStructureExtractor.extract(fixture("cjk-mixed.pdf"))
75+
.toMarkdown();
76+
// CJK 字形在不同 iText 渲染下可能微变;只断言保留中文段存在
77+
assertThat(md).contains("标题").contains("正文");
78+
}
79+
80+
@Test
81+
@Disabled("enable after single-page-plain.pdf fixture is committed")
82+
@DisplayName("contract: extractWithReport counts success in metrics")
83+
void extractWithReportPopulatesMetrics() throws Exception {
84+
// INSTANCE 是进程级共享,单测之间可能互有污染;
85+
// 只断言"调用后 successes 计数严格 +1"。
86+
long successBefore = ExtractorMetrics.INSTANCE.snapshot()
87+
.getOrDefault("successes", 0L);
88+
89+
ExtractReport report = PdfStructureExtractor.extractWithReport(
90+
fixture("single-page-plain.pdf"),
91+
PdfExtractionProperties.defaults());
92+
93+
assertThat(report).isNotNull();
94+
assertThat(report.document).isNotNull();
95+
long successAfter = ExtractorMetrics.INSTANCE.snapshot()
96+
.getOrDefault("successes", 0L);
97+
assertThat(successAfter - successBefore).isEqualTo(1L);
98+
}
99+
100+
@Test
101+
@DisplayName("contract: NOT_FOUND surfaces as ExtractionException")
102+
void missingFileSurfacesAsNotFound() {
103+
File missing = new File("target/does-not-exist.pdf");
104+
assertThatThrownBy(() -> PdfStructureExtractor.extract(missing))
105+
.isInstanceOf(ExtractionException.class)
106+
.extracting(e -> ((ExtractionException) e).getCode())
107+
.isEqualTo(ExtractionException.Code.NOT_FOUND);
108+
}
109+
110+
@Test
111+
@Disabled("enable after encrypted.pdf fixture is committed")
112+
@DisplayName("contract: ENCRYPTED PDF is classified")
113+
void encryptedPdfIsClassified() {
114+
// 由 RobustnessTest 等单测覆盖生成加密 PDF;IT 这里只断言默认 props 行为
115+
// ——遇到 ENCRYPTED 抛 ExtractionException(Code.ENCRYPTED)
116+
File encrypted = fixture("encrypted.pdf");
117+
assertThatThrownBy(() -> PdfStructureExtractor.extract(encrypted))
118+
.isInstanceOf(ExtractionException.class)
119+
.extracting(e -> ((ExtractionException) e).getCode())
120+
.isEqualTo(ExtractionException.Code.ENCRYPTED);
121+
}
122+
123+
private static String readSnapshot(String name) throws Exception {
124+
URL url = MarkdownContractIT.class.getResource(SNAPSHOTS + "/" + name + ".md");
125+
assertThat((Object) url).as("snapshot %s.md missing", name).isNotNull();
126+
return new String(java.nio.file.Files.readAllBytes(Paths.get(url.getPath())), "UTF-8");
127+
}
128+
}

0 commit comments

Comments
 (0)