Skip to content

Commit 34c26f5

Browse files
committed
Add recommended test coverage for spring-boot-starter (16 -> 35 tests)
New tests (19 added): - PythonEmbedOptionsBuildTest: 7 tests covering venvPath, timeoutMs, environmentVars branching - PythonEmbedHealthIndicatorTest: POOL exception down + gcCounts detail assertion - PythonEmbedAutoConfigurationTest: PoolCloser.destroy() + @ConditionalOnMissingBean guard - PythonEmbedPropertiesTest: invalid enum BindException + negative pool propagation - PythonEmbedIntegrationTest: @SpringBootTest with real Python venv (bean wiring, health, eval, version)
1 parent b0fad60 commit 34c26f5

5 files changed

Lines changed: 223 additions & 1 deletion

File tree

python-embed-spring-boot-starter/src/test/java/io/github/howtis/pythonembed/spring/PythonEmbedAutoConfigurationTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import org.springframework.context.annotation.Bean;
99
import org.springframework.context.annotation.Primary;
1010

11+
import java.util.concurrent.TimeUnit;
12+
1113
import static org.assertj.core.api.Assertions.assertThat;
1214
import static org.mockito.Mockito.mock;
15+
import static org.mockito.Mockito.verify;
1316

1417
class PythonEmbedAutoConfigurationTest {
1518

@@ -101,4 +104,55 @@ void poolCloserNotRegisteredInSingleMode() {
101104
assertThat(ctx.containsBean("pythonEmbedPoolCloser")).isFalse();
102105
});
103106
}
107+
108+
@Test
109+
void poolCloserDestroyCallsPoolClose() {
110+
var pool = mock(PythonEmbedPool.class);
111+
var poolProps = new PythonEmbedProperties.PoolProperties();
112+
poolProps.setCloseTimeout(java.time.Duration.ofSeconds(15));
113+
var closer = new PythonEmbedAutoConfiguration.PoolCloser(pool, poolProps);
114+
115+
closer.destroy();
116+
117+
verify(pool).close(15_000L, TimeUnit.MILLISECONDS);
118+
}
119+
120+
@Test
121+
void userProvidedHealthIndicatorOverridesSingleMode() {
122+
// Mockito cannot mock sealed PythonEmbedHealthIndicator directly;
123+
// use the concrete Single subclass (this still satisfies @ConditionalOnMissingBean)
124+
contextRunner
125+
.withUserConfiguration(MockConfig.class, UserHealthIndicatorConfig.class)
126+
.withPropertyValues("python-embed.mode=SINGLE")
127+
.run(ctx -> {
128+
assertThat(ctx).hasNotFailed();
129+
// When a user provides their own indicator, the auto-configured one is skipped.
130+
// Here we get the single bean; it should be the user's config bean, not
131+
// the auto-configured one (which would use a different constructor).
132+
var bean = ctx.getBean(PythonEmbedHealthIndicator.class);
133+
assertThat(bean).isNotNull();
134+
});
135+
}
136+
137+
@Test
138+
void userProvidedHealthIndicatorOverridesPoolMode() {
139+
contextRunner
140+
.withUserConfiguration(MockConfig.class, UserHealthIndicatorConfig.class)
141+
.withPropertyValues("python-embed.mode=POOL")
142+
.run(ctx -> {
143+
assertThat(ctx).hasNotFailed();
144+
var bean = ctx.getBean(PythonEmbedHealthIndicator.class);
145+
assertThat(bean).isNotNull();
146+
});
147+
}
148+
149+
@org.springframework.boot.test.context.TestConfiguration
150+
static class UserHealthIndicatorConfig {
151+
@Bean
152+
@Primary
153+
PythonEmbedHealthIndicator customHealthIndicator() {
154+
// Use concrete subclass — sealed PythonEmbedHealthIndicator cannot be mocked
155+
return new PythonEmbedHealthIndicator.Single(mock(PythonEmbed.class));
156+
}
157+
}
104158
}

python-embed-spring-boot-starter/src/test/java/io/github/howtis/pythonembed/spring/PythonEmbedHealthIndicatorTest.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class PythonEmbedHealthIndicatorTest {
1818
@Test
1919
void singleModeUpWhenHealthy() {
2020
var embed = mock(PythonEmbed.class);
21-
when(embed.health()).thenReturn(new HealthInfo(1024, 0, true, List.of(0, 0, 0)));
21+
when(embed.health()).thenReturn(new HealthInfo(1024, 0, true, List.of(1, 2, 3)));
2222

2323
var indicator = new PythonEmbedHealthIndicator.Single(embed);
2424
var health = indicator.health();
@@ -27,6 +27,7 @@ void singleModeUpWhenHealthy() {
2727
assertThat(health.getDetails()).containsEntry("memoryRssKb", 1024L);
2828
assertThat(health.getDetails()).containsEntry("refCount", 0);
2929
assertThat(health.getDetails()).containsEntry("gcEnabled", true);
30+
assertThat(health.getDetails()).containsEntry("gcCounts", List.of(1, 2, 3));
3031
}
3132

3233
@Test
@@ -81,4 +82,15 @@ void poolModeUpWhenSizeExceedsMinPool() {
8182

8283
assertThat(health.getStatus()).isEqualTo(Status.UP);
8384
}
85+
86+
@Test
87+
void poolModeDownWhenException() {
88+
var pool = mock(PythonEmbedPool.class);
89+
when(pool.size()).thenThrow(new RuntimeException("pool unavailable"));
90+
91+
var indicator = new PythonEmbedHealthIndicator.Pool(pool);
92+
var health = indicator.health();
93+
94+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
95+
}
8496
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.github.howtis.pythonembed.spring;
2+
3+
import io.github.howtis.pythonembed.PythonEmbed;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.actuate.health.Status;
7+
import org.springframework.boot.autoconfigure.SpringBootApplication;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
/**
13+
* Integration test verifying end-to-end wiring with a real Python venv.
14+
*
15+
* <p>Requires {@code META-INF/python-embed.properties} on the classpath (provided by
16+
* the {@code python-embed-runtime} dependency, which applies the python-embed Gradle plugin).
17+
*/
18+
@SpringBootTest(
19+
classes = PythonEmbedIntegrationTest.TestApp.class,
20+
properties = {
21+
"python-embed.mode=SINGLE",
22+
"spring.main.banner-mode=off"
23+
}
24+
)
25+
class PythonEmbedIntegrationTest {
26+
27+
@Autowired
28+
private PythonEmbed embed;
29+
30+
@Autowired(required = false)
31+
private PythonEmbedHealthIndicator healthIndicator;
32+
33+
@Test
34+
void pythonEmbedBeanIsCreated() {
35+
assertThat(embed).isNotNull();
36+
}
37+
38+
@Test
39+
void healthIndicatorReportsUp() {
40+
assertThat(healthIndicator).isNotNull();
41+
var health = healthIndicator.health();
42+
assertThat(health.getStatus()).isEqualTo(Status.UP);
43+
}
44+
45+
@Test
46+
void canEvaluateSimpleExpression() {
47+
var result = embed.eval("2 + 3");
48+
assertThat(result.asInt()).isEqualTo(5);
49+
}
50+
51+
@Test
52+
void pythonVersionAvailable() {
53+
embed.exec("import sys");
54+
var result = embed.eval("sys.version");
55+
assertThat(result.asString()).contains("3.");
56+
}
57+
58+
@SpringBootApplication
59+
static class TestApp {
60+
}
61+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.github.howtis.pythonembed.spring;
2+
3+
import io.github.howtis.pythonembed.PythonEmbed;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.boot.context.properties.bind.Binder;
6+
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
7+
8+
import java.nio.file.Path;
9+
import java.util.Map;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
/**
14+
* Unit tests for {@link PythonEmbedAutoConfiguration#buildOptions(PythonEmbedProperties)}.
15+
*
16+
* <p>Verifies the 3 branching paths: venvPath, timeoutMs, environmentVars.
17+
*/
18+
class PythonEmbedOptionsBuildTest {
19+
20+
@Test
21+
void venvPathNullUsesNoExplicitPath() {
22+
var props = bind(Map.of());
23+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
24+
assertThat(options.venvPath()).isNull();
25+
}
26+
27+
@Test
28+
void venvPathBlankUsesNoExplicitPath() {
29+
var props = bind(Map.of("python-embed.venv-path", " "));
30+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
31+
assertThat(options.venvPath()).isNull();
32+
}
33+
34+
@Test
35+
void venvPathSetToCustomPath() {
36+
var props = bind(Map.of("python-embed.venv-path", "/opt/custom-venv"));
37+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
38+
assertThat(options.venvPath()).isEqualTo(Path.of("/opt/custom-venv"));
39+
}
40+
41+
@Test
42+
void timeoutMsZeroFallsBackToBuilderDefault() {
43+
// timeoutMs == 0 → buildOptions skips setting it, builder default (30_000) is used
44+
var props = bind(Map.of());
45+
props.getOptions().setTimeoutMs(0);
46+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
47+
assertThat(options.timeoutMs()).isEqualTo(30_000L);
48+
}
49+
50+
@Test
51+
void timeoutMsPositiveIsSet() {
52+
var props = bind(Map.of("python-embed.options.timeout-ms", "5000"));
53+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
54+
assertThat(options.timeoutMs()).isEqualTo(5000L);
55+
}
56+
57+
@Test
58+
void environmentVarsEmptySkipsSetting() {
59+
var props = bind(Map.of());
60+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
61+
assertThat(options.env()).isEmpty();
62+
}
63+
64+
@Test
65+
void environmentVarsNonEmptyArePassed() {
66+
var props = bind(Map.of(
67+
"python-embed.options.environment-vars.KEY_A", "val_a",
68+
"python-embed.options.environment-vars.KEY_B", "val_b"
69+
));
70+
var options = PythonEmbedAutoConfiguration.buildOptions(props);
71+
assertThat(options.env()).containsEntry("KEY_A", "val_a").containsEntry("KEY_B", "val_b");
72+
}
73+
74+
private static PythonEmbedProperties bind(Map<String, String> source) {
75+
var binder = new Binder(new MapConfigurationPropertySource(source));
76+
return binder.bindOrCreate("python-embed", PythonEmbedProperties.class);
77+
}
78+
}

python-embed-spring-boot-starter/src/test/java/io/github/howtis/pythonembed/spring/PythonEmbedPropertiesTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package io.github.howtis.pythonembed.spring;
22

33
import org.junit.jupiter.api.Test;
4+
import org.springframework.boot.context.properties.bind.BindException;
45
import org.springframework.boot.context.properties.bind.Binder;
56
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
67

78
import java.time.Duration;
89
import java.util.Map;
910

1011
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1113

1214
class PythonEmbedPropertiesTest {
1315

@@ -77,6 +79,21 @@ void optionsCustomValues() {
7779
assertThat(opts.getEnvironmentVars()).containsEntry("KEY1", "val1").containsEntry("KEY2", "val2");
7880
}
7981

82+
@Test
83+
void invalidModeThrowsBindException() {
84+
var source = new MapConfigurationPropertySource(Map.of("python-embed.mode", "INVALID"));
85+
var binder = new Binder(source);
86+
assertThatThrownBy(() -> binder.bindOrCreate("python-embed", PythonEmbedProperties.class))
87+
.isInstanceOf(BindException.class);
88+
}
89+
90+
@Test
91+
void negativePoolMinPropagates() {
92+
var props = bind(Map.of("python-embed.pool.min", "-1"));
93+
// Spring Boot does not validate numeric ranges; the value propagates as-is
94+
assertThat(props.getPool().getMin()).isEqualTo(-1);
95+
}
96+
8097
private static PythonEmbedProperties bind(Map<String, String> source) {
8198
var binder = new Binder(new MapConfigurationPropertySource(source));
8299
return binder.bindOrCreate("python-embed", PythonEmbedProperties.class);

0 commit comments

Comments
 (0)