Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/vortex-test-common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ jobs:
env:
TEST_VORTEX_DEBUG: ${{ vars.TEST_VORTEX_DEBUG }}

- name: Upload test artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: always()
with:
name: ${{github.job}}-${{ matrix.batch }}-test-artifacts
path: .vortex/tests/.logs
include-hidden-files: true
if-no-files-found: ignore

- name: Upload coverage report as an artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
Expand Down
30 changes: 30 additions & 0 deletions .vortex/tests/phpunit/Functional/FunctionalTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ protected function tearDown(): void {

$test_failed = $this->status() instanceof Failure || $this->status() instanceof Error;

// Collect SUT test artifacts before cleanup.
$this->collectSutArtifacts($test_failed);

if ($test_failed) {
$this->logNote('Skipping cleanup as test has failed.');
$this->log(static::locationsInfo());
Expand All @@ -79,6 +82,33 @@ protected function tearDown(): void {
parent::tearDown();
}

protected function collectSutArtifacts(bool $test_failed): void {
// On failure, containers are still running but syncToHost() may not have
// been called before the failure. Try to sync artifacts from the container.
if ($test_failed && !$this->volumesMounted()) {
try {
$this->syncToHost('.logs');
}
catch (\Throwable) {
// Ignore - container may not be running or .logs may not exist.
}
}

$sut_logs = static::$sut . '/.logs';
if (!is_dir($sut_logs)) {
return;
}

$class_name = (new \ReflectionClass($this))->getShortName();
$test_id = $class_name . '_' . $this->name() . '_' . uniqid();
$test_id = (string) preg_replace('/[^a-zA-Z0-9_-]/', '_', $test_id);

$dest = dirname(__DIR__, 2) . '/.logs/sut/' . $test_id;
File::copy($sut_logs, $dest);

$this->logNote('SUT test artifacts copied to: ' . $dest);
Comment on lines +102 to +109
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# Find the File class being used
rg 'class File' --type php -A 5 | head -50

Repository: drevops/vortex

Length of output: 40


🏁 Script executed:

# Look for File class imports or usage
rg 'use.*File' .vortex/tests/phpunit/Functional/FunctionalTestCase.php

Repository: drevops/vortex

Length of output: 85


🏁 Script executed:

# Search for File::copy definition
rg 'function copy\(' --type php

Repository: drevops/vortex

Length of output: 40


🏁 Script executed:

# Check if File class is from a composer package
cat composer.json | jq '.require, .require-dev' 2>/dev/null || find . -name "composer.json" -type f -exec cat {} \; | head -100

Repository: drevops/vortex

Length of output: 4691


🌐 Web query:

alexskrypnyk/file 0.19.0 File::copy() documentation

💡 Result:

For alexskrypnyk/file v0.19.0 (released 2025-12-23), the README documents AlexSkrypnyk\File\File::copy() as a static utility to copy a file or a directory (the example shows copying a directory recursively). It’s intended to be used inside a try/catch for FileException. [1]

Example (from the v0.19.0 README):

use AlexSkrypnyk\File\Exception\FileException;
use AlexSkrypnyk\File\File;

try {
  File::copy('/path/to/source', '/path/to/destination');
} catch (FileException $e) {
  echo $e->getMessage();
}
``` [1]

**Where it’s listed:**
- “Available Methods” table: **`copy()` — Copy file or directory.** [1]

**Source:** Packagist README for `alexskrypnyk/file` **0.19.0**. [1]

References:  
[1] https://packagist.org/packages/alexskrypnyk/file

---

🌐 Web query:

`github AlexSkrypnyk file php copy method parent directories`

💡 Result:

I couldn’t find a specific `File.php` (or a `copy()` implementation) in an `AlexSkrypnyk` GitHub repo from that query, but in plain PHP the key point is: **`copy($from, $to)` will fail if the destination directory doesn’t exist**—it does not create parent directories for you. [1]

Create the parent dirs first (recursively), then copy:

```php
$from = $src;
$to   = $dst;

$dir = dirname($to);
if (!is_dir($dir)) {
  mkdir($dir, 0777, true); // recursive parent directories
}

if (!copy($from, $to)) {
  throw new RuntimeException("Failed to copy $from to $to");
}
  • mkdir(..., $recursive=true) creates nested/parent directories. [2]
  • copy() copies/overwrites the destination file, but won’t create missing directories. [1]

Sources: [1] [2]


Defensive directory creation + copy error handling.

The .logs/sut directory must be created explicitly before copying, and File::copy() throws FileException on failure. In tearDown(), artifact logging failures should not mask test results. Ensure the destination directory exists and handle copy exceptions gracefully.

♻️ Suggested hardening
-    $dest = dirname(__DIR__, 2) . '/.logs/sut/' . $test_id;
-    File::copy($sut_logs, $dest);
+    $dest_root = dirname(__DIR__, 2) . '/.logs/sut';
+    if (!is_dir($dest_root) && !mkdir($dest_root, 0777, TRUE) && !is_dir($dest_root)) {
+      $this->logNote('Unable to create SUT artifacts directory: ' . $dest_root);
+      return;
+    }
+    $dest = $dest_root . '/' . $test_id;
+    try {
+      File::copy($sut_logs, $dest);
+    }
+    catch (\Throwable $e) {
+      $this->logNote('Failed to copy SUT artifacts: ' . $e->getMessage());
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$class_name = (new \ReflectionClass($this))->getShortName();
$test_id = $class_name . '_' . $this->name() . '_' . uniqid();
$test_id = (string) preg_replace('/[^a-zA-Z0-9_-]/', '_', $test_id);
$dest = dirname(__DIR__, 2) . '/.logs/sut/' . $test_id;
File::copy($sut_logs, $dest);
$this->logNote('SUT test artifacts copied to: ' . $dest);
$class_name = (new \ReflectionClass($this))->getShortName();
$test_id = $class_name . '_' . $this->name() . '_' . uniqid();
$test_id = (string) preg_replace('/[^a-zA-Z0-9_-]/', '_', $test_id);
$dest_root = dirname(__DIR__, 2) . '/.logs/sut';
if (!is_dir($dest_root) && !mkdir($dest_root, 0777, TRUE) && !is_dir($dest_root)) {
$this->logNote('Unable to create SUT artifacts directory: ' . $dest_root);
return;
}
$dest = $dest_root . '/' . $test_id;
try {
File::copy($sut_logs, $dest);
}
catch (\Throwable $e) {
$this->logNote('Failed to copy SUT artifacts: ' . $e->getMessage());
return;
}
$this->logNote('SUT test artifacts copied to: ' . $dest);
🤖 Prompt for AI Agents
In @.vortex/tests/phpunit/Functional/FunctionalTestCase.php around lines 102 -
109, In tearDown() where $dest is built and File::copy($sut_logs, $dest) is
called, ensure the destination directory exists (create dirname($dest) with
appropriate permissions) before copying, and wrap the File::copy call in a
try/catch that catches the FileException (or \Exception) so copy failures are
logged via $this->logNote or a warning but not rethrown (so they don't mask test
results); reference the $dest variable, File::copy, the surrounding tearDown()
method and $this->logNote when making this change.

}

/**
* {@inheritdoc}
*/
Expand Down