@@ -186,4 +186,95 @@ public function test_form_type_class_options_passed_to_parent(): void
186186 $ constraint = new FormTypeClass (message: 'different message option ' );
187187 $ this ->assertEquals ('different message option ' , $ constraint ->message );
188188 }
189+
190+ // --- Deterministic single-branch coverage (kills surviving mutants) ---
191+
192+ /**
193+ * Captures the messages of every violation raised for a single validate() call.
194+ *
195+ * @return list<string>
196+ */
197+ private function captureMessages (mixed $ value , Constraint $ constraint , iterable $ formTypes = [new TestType ()]): array
198+ {
199+ $ validator = new FormTypeClassValidator ($ formTypes );
200+
201+ $ messages = [];
202+ $ builder = $ this ->createStub (ConstraintViolationBuilderInterface::class);
203+ $ builder ->method ('setParameter ' )->willReturn ($ builder );
204+ $ builder ->method ('atPath ' )->willReturn ($ builder );
205+
206+ $ context = $ this ->createStub (ExecutionContextInterface::class);
207+ $ context ->method ('buildViolation ' )->willReturnCallback (static function (string $ message ) use (&$ messages , $ builder ): ConstraintViolationBuilderInterface {
208+ $ messages [] = $ message ;
209+
210+ return $ builder ;
211+ });
212+
213+ $ validator ->initialize ($ context );
214+ $ validator ->validate ($ value , $ constraint );
215+
216+ return $ messages ;
217+ }
218+
219+ public function test_empty_string_value_raises_no_violation (): void
220+ {
221+ // Kills LogicalNot (line 34) and ReturnRemoval (line 35): an empty string is falsy and must
222+ // return immediately. Without the early return it reaches ClassNameValidator, which throws on
223+ // the non-existent class '' and produces a spurious violation.
224+ $ messages = $ this ->captureMessages ('' , new FormTypeClass ());
225+
226+ self ::assertSame ([], $ messages );
227+ }
228+
229+ public function test_non_string_value_throws_invalid_argument (): void
230+ {
231+ // Kills LogicalNot (line 37) and Throw_ (line 38): a non-string value must throw before any
232+ // validation runs.
233+ $ validator = new FormTypeClassValidator ([new TestType ()]);
234+ $ validator ->initialize ($ this ->executionContextMock );
235+
236+ $ this ->expectException (InvalidArgumentException::class);
237+ $ validator ->validate (new TestType (), new FormTypeClass ());
238+ }
239+
240+ public function test_unexpected_constraint_type_throws_invalid_argument (): void
241+ {
242+ // Kills InstanceOf_ / LogicalNot (line 40) and Throw_ (line 41): a constraint that is not a
243+ // FormTypeClass must throw.
244+ $ validator = new FormTypeClassValidator ([new TestType ()]);
245+ $ validator ->initialize ($ this ->executionContextMock );
246+
247+ $ this ->expectException (InvalidArgumentException::class);
248+ $ validator ->validate (TestType::class, new class extends Constraint {
249+ });
250+ }
251+
252+ public function test_class_not_in_form_types_raises_message_violation (): void
253+ {
254+ // Kills LogicalNot (line 46) and MethodCallRemoval (line 47): a real class that is not among
255+ // the configured form types must raise exactly `message`.
256+ $ constraint = new FormTypeClass ();
257+ $ messages = $ this ->captureMessages (__CLASS__ , $ constraint );
258+
259+ self ::assertSame ([$ constraint ->message ], $ messages );
260+ }
261+
262+ public function test_non_class_string_raises_exception_message_violation (): void
263+ {
264+ // Kills MethodCallRemoval (line 53): a string that is not a class makes ClassNameValidator
265+ // throw; the catch block must raise a violation carrying the exception message.
266+ $ messages = $ this ->captureMessages ('NotAClass ' , new FormTypeClass ());
267+
268+ self ::assertCount (1 , $ messages );
269+ self ::assertStringContainsString ('NotAClass ' , $ messages [0 ]);
270+ }
271+
272+ public function test_valid_form_type_class_raises_no_violation (): void
273+ {
274+ // Confirms the happy path: a configured form type must produce no violation (pins line 46 in
275+ // the other direction).
276+ $ messages = $ this ->captureMessages (TestType::class, new FormTypeClass ());
277+
278+ self ::assertSame ([], $ messages );
279+ }
189280}
0 commit comments