@@ -316,6 +316,112 @@ public function testForceCacheClearRemovesAstAndTargetIncrementalState(): void
316316 self ::assertDirectoryDoesNotExist ($ incrementalDirectory );
317317 }
318318
319+ private function prepareNativeStub (): void
320+ {
321+ $ stub = $ this ->directory . '/provider.stub.php ' ;
322+ rename ($ this ->provider , $ stub );
323+ $ this ->provider = $ stub ;
324+ file_put_contents ($ stub , '<?php namespace Incremental; function answer(int $value = 42): int {} ' );
325+ file_put_contents ($ this ->consumer , '<?php function main(): int { return \\Incremental \\answer(); } ' );
326+ }
327+
328+ public function testStubDeclarationsAreGeneratedWithoutConvertingStubBodies (): void
329+ {
330+ $ this ->prepareNativeStub ();
331+ $ first = $ this ->convertProject ();
332+ $ header = $ first ->getDeclarationHeaderFile ($ this ->provider );
333+ self ::assertFileExists ($ header );
334+ self ::assertStringContainsString ('php_incremental__answer( ' , file_get_contents ($ header ));
335+ self ::assertStringContainsString ('#include < ' . basename ($ header ) . '> ' ,
336+ file_get_contents ($ first ->getDeclarationHeaderFile ($ this ->consumer )));
337+ self ::assertFileDoesNotExist ($ this ->invoke ($ first , 'getCppFile ' , $ this ->provider ));
338+ self ::assertFileExists ($ first ->getArgInfoHeaderFile ($ this ->provider ));
339+ $ extension = $ this ->buildDirectory . '/extension-incremental.cc ' ;
340+ $ extensionCode = file_get_contents ($ extension );
341+ self ::assertStringContainsString ('ZEND_FUNCTION(incremental_answer) ' , $ extensionCode );
342+ self ::assertStringContainsString ('#include < ' . basename ($ first ->getArgInfoHeaderFile ($ this ->provider )) . '> ' , $ extensionCode );
343+ $ state = json_decode (file_get_contents ($ this ->buildDirectory . '/cache/incremental/incremental/build-state.json ' ), true );
344+ self ::assertFalse ($ state ['files ' ][$ this ->provider ]['emitsTranslationUnit ' ]);
345+ self ::assertContains ($ this ->provider , $ state ['files ' ][$ this ->consumer ]['dependencies ' ]);
346+ $ second = $ this ->convertProject ();
347+ self ::assertFalse ($ this ->invoke ($ second , 'shouldRegeneratePhpFile ' , $ this ->provider ));
348+ self ::assertFalse ($ this ->invoke ($ second , 'shouldRegeneratePhpFile ' , $ this ->consumer ));
349+ self ::assertSame ($ extensionCode , file_get_contents ($ extension ));
350+ }
351+
352+ public function testStubChangePreservingMtimeInvalidatesTheConsumer (): void
353+ {
354+ $ this ->prepareNativeStub ();
355+ $ this ->convertProject ();
356+ $ extension = $ this ->buildDirectory . '/extension-incremental.cc ' ;
357+ $ oldCode = file_get_contents ($ extension );
358+ $ mtime = filemtime ($ this ->provider );
359+ file_put_contents ($ this ->provider , str_replace ('42 ' , '43 ' , file_get_contents ($ this ->provider )));
360+ touch ($ this ->provider , $ mtime );
361+ clearstatcache ();
362+ $ second = $ this ->convertProject ();
363+ self ::assertTrue ($ this ->invoke ($ second , 'shouldRegeneratePhpFile ' , $ this ->provider ));
364+ self ::assertTrue ($ this ->invoke ($ second , 'shouldRegeneratePhpFile ' , $ this ->consumer ));
365+ self ::assertNotSame ($ oldCode , file_get_contents ($ extension ));
366+ self ::assertStringContainsString ('43 ' , file_get_contents ($ extension ));
367+ }
368+
369+ public function testMissingStubHeaderIsRegeneratedAndInvalidatesConsumers (): void
370+ {
371+ $ this ->prepareNativeStub ();
372+ $ first = $ this ->convertProject ();
373+ unlink ($ first ->getDeclarationHeaderFile ($ this ->provider ));
374+ $ second = $ this ->convertProject ();
375+ self ::assertFileExists ($ second ->getDeclarationHeaderFile ($ this ->provider ));
376+ self ::assertTrue ($ this ->invoke ($ second , 'shouldRegeneratePhpFile ' , $ this ->consumer ));
377+ }
378+
379+ public function testMissingStubArginfoIsRegeneratedAndInvalidatesConsumers (): void
380+ {
381+ $ this ->prepareNativeStub ();
382+ $ first = $ this ->convertProject ();
383+ unlink ($ first ->getArgInfoHeaderFile ($ this ->provider ));
384+ $ second = $ this ->convertProject ();
385+ self ::assertFileExists ($ second ->getArgInfoHeaderFile ($ this ->provider ));
386+ self ::assertTrue ($ this ->invoke ($ second , 'shouldRegeneratePhpFile ' , $ this ->consumer ));
387+ }
388+
389+ public function testImportedClassMetadataAndCallbacksStayInTheExtension (): void
390+ {
391+ $ this ->prepareNativeStub ();
392+ file_put_contents ($ this ->provider , <<<'PHP'
393+ <?php
394+ /** @import-library */
395+ namespace Incremental;
396+ function answer(int $value = 42): int {}
397+ final class Counter
398+ {
399+ public int $value = 0;
400+ public function add(int $delta): int {}
401+ }
402+ PHP);
403+ file_put_contents ($ this ->consumer , <<<'PHP'
404+ <?php
405+ function main(): int
406+ {
407+ $counter = new \Incremental\Counter();
408+ return $counter->add(1) + \Incremental\answer();
409+ }
410+ PHP);
411+ $ first = $ this ->convertProject ();
412+ $ arginfo = $ first ->getArgInfoHeaderFile ($ this ->provider );
413+ $ extension = $ this ->buildDirectory . '/extension-incremental.cc ' ;
414+ $ extensionCode = file_get_contents ($ extension );
415+ self ::assertStringContainsString ('php_register_class_Incremental_Counter ' , file_get_contents ($ arginfo ));
416+ self ::assertStringContainsString ('ZEND_METHOD(Incremental_Counter, add) ' , $ extensionCode );
417+ self ::assertStringContainsString ('php_incremental__counter__add(this_, arg_delta) ' , $ extensionCode );
418+ self ::assertStringContainsString ('#include < ' . basename ($ arginfo ) . '> ' , $ extensionCode );
419+ self ::assertStringNotContainsString (basename ($ arginfo ), file_get_contents ($ this ->invoke ($ first , 'getCppFile ' , $ this ->consumer )));
420+ self ::assertFileDoesNotExist ($ this ->invoke ($ first , 'getCppFile ' , $ this ->provider ));
421+ $ this ->convertProject ();
422+ self ::assertSame ($ extensionCode , file_get_contents ($ extension ));
423+ }
424+
319425 private function convertProject (): CompilerTest
320426 {
321427 global $ translator ;
0 commit comments