From 0f8ca867fd201eeeca3576efe88fa258cfba8ee2 Mon Sep 17 00:00:00 2001
From: Steve Boyd <emteknetnz@gmail.com>
Date: Fri, 1 Jul 2022 11:03:43 +1200
Subject: [PATCH] FIX Update getInstallerVersion

---
 .github/workflows/ci.yml |  27 +++++++
 .gitignore               |   1 +
 action.php               |   8 +--
 job_creator.php          |  58 ++++++++++-----
 phpunit.xml              |   8 +++
 tests/JobCreatorTest.php | 149 +++++++++++++++++++++++++++++++++++++++
 tests/bootstrap.php      |   4 ++
 7 files changed, 233 insertions(+), 22 deletions(-)
 create mode 100644 .github/workflows/ci.yml
 create mode 100644 .gitignore
 create mode 100644 phpunit.xml
 create mode 100644 tests/JobCreatorTest.php
 create mode 100644 tests/bootstrap.php

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4e47222
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: CI
+
+on:
+  push:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  ci:
+    name: CI
+    runs-on: ubuntu-latest
+    steps:
+
+      - name: Checkout code
+        uses: actions/checkout@7884fcad6b5d53d10323aee724dc68d8b9096a2e # v2.4.2
+
+      - name: Install PHP
+        uses: shivammathur/setup-php@3eda58347216592f618bb1dff277810b6698e4ca # v2.19.1
+        with:
+          php-version: 8.1
+          extensions: yaml
+
+      - name: Install PHPUnit
+        run: wget https://phar.phpunit.de/phpunit-9.5.phar
+
+      - name: PHPUnit
+        run: php phpunit-9.5.phar --verbose --colors=always
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..165765a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.phpunit.result.cache
diff --git a/action.php b/action.php
index 52ac606..dabbdbb 100644
--- a/action.php
+++ b/action.php
@@ -4,10 +4,6 @@
 include 'job_creator.php';
 
 // Reads inputs.yml and creates a new json matrix
-$inputs = yaml_parse(file_get_contents('__inputs.yml'));
-if ($inputs === false) {
-    echo 'Unable to parse __inputs.yml';
-    exit(1);
-}
+$yml = file_get_contents('__inputs.yml');
 $jobCreator = new JobCreator();
-echo $jobCreator->createJson($inputs);
+echo $jobCreator->createJson($yml);
diff --git a/job_creator.php b/job_creator.php
index 53a0264..d7e6e94 100644
--- a/job_creator.php
+++ b/job_creator.php
@@ -13,23 +13,29 @@ public function getInstallerVersion(string $githubRepository, string $branch): s
         if (in_array($repo, NO_INSTALLER_REPOS)) {
             return '';
         }
-        // e.g. ['4', '11']
-        $portions = explode('.', $branch);
-        if (count($portions) == 1) {
-            return '4.x-dev';
+        // e.g. pulls/4.10/some-bugfix or pulls/4/some-feature
+        if (preg_match('#^pulls/([0-9\.]+)/#', $branch, $matches)) {
+            $branch = $matches[1];
         }
-        if (in_array($repo, LOCKSTEPED_REPOS)) {
-            return '4.' . $portions[1] . '.x-dev';
-        } else {
-            // use the latest minor version of installer
-            $installerVersions = array_keys(INSTALLER_TO_PHP_VERSIONS);
-            // remove '4' version
-            $installerVersions = array_diff($installerVersions, ['4']);
-            // get the minor portions of the verisons e.g. [9, 10, 11]
-            $minorPortions = array_map(fn($portions) => (int) explode('.', $portions)[1], $installerVersions);
-            sort($minorPortions);
-            return '4.' . $minorPortions[count($minorPortions) - 1] . '.x-dev';
+        // e.g. 4.10-release
+        $branch = preg_replace('#^([0-9\.]+)-release$#', '$1', $branch);
+        if (in_array($repo, LOCKSTEPED_REPOS) && is_numeric($branch)) {
+            // e.g. ['4', '11']
+            $portions = explode('.', $branch);
+            if (count($portions) == 1) {
+                return '4.x-dev';
+            } else {
+                return '4.' . $portions[1] . '.x-dev';
+            }
         }
+        // use the latest minor version of installer
+        $installerVersions = array_keys(INSTALLER_TO_PHP_VERSIONS);
+        // remove '4' version
+        $installerVersions = array_diff($installerVersions, ['4']);
+        // get the minor portions of the verisons e.g. [9, 10, 11]
+        $minorPortions = array_map(fn($portions) => (int) explode('.', $portions)[1], $installerVersions);
+        sort($minorPortions);
+        return '4.' . $minorPortions[count($minorPortions) - 1] . '.x-dev';
     }
     
     public function createJob(int $phpIndex, array $opts): array
@@ -180,8 +186,28 @@ private function buildDynamicMatrix(
         return $matrix;
     }
 
-    public function createJson(array $inputs): string
+    public function getInputs(string $yml): array
+    {
+        $message = 'Failed to parse yml';
+        try {
+            $inputs = yaml_parse($yml);
+        } catch (Exception $e) {
+            throw new Exception($message);
+        }
+        if (!$inputs) {
+            throw new Exception($message);
+        }
+        if (array_key_exists('github_my_ref', $inputs)) {
+            if (!preg_match("#github_my_ref: *'#", $yml)) {
+                throw new Exception('github_my_ref needs to be surrounded by single-quotes');
+            }
+        }
+        return $inputs;
+    }
+
+    public function createJson(string $yml): string
     {
+        $inputs = $this->getInputs($yml);
         // $myRef will either be a branch for push (i.e cron) and pull-request (target branch), or a semver tag
         $myRef = $inputs['github_my_ref'];
         $isTag = preg_match('#^[0-9]+\.[0-9]+\.[0-9]+$#', $myRef, $m);
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..f5e111b
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="tests/bootstrap.php" colors="true">
+    <testsuites>
+        <testsuite name="Default">
+            <directory>tests</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
diff --git a/tests/JobCreatorTest.php b/tests/JobCreatorTest.php
new file mode 100644
index 0000000..979db8a
--- /dev/null
+++ b/tests/JobCreatorTest.php
@@ -0,0 +1,149 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class JobCreatorTest extends TestCase
+{
+    /**
+     * @dataProvider provideGetInstallerVersion
+     */
+    public function testGetInstallerVersion(
+        string $githubRepository,
+        string $branch,
+        string $expected
+    ): void {
+        $creator = new JobCreator();
+        $actual = $creator->getInstallerVersion($githubRepository, $branch);
+        $this->assertSame($expected, $actual);
+    }
+
+    private function getLatestInstallerVersion(): string
+    {
+        $versions = array_keys(INSTALLER_TO_PHP_VERSIONS);
+        natsort($versions);
+        $versions = array_reverse($versions);
+        return $versions[0];
+    }
+
+    public function provideGetInstallerVersion(): array
+    {
+        $latest = $this->getLatestInstallerVersion() . '.x-dev';
+        return [
+            // no-installer repo
+            ['myaccount/recipe-cms', '4', ''],
+            ['myaccount/recipe-cms', '4.10', ''],
+            ['myaccount/recipe-cms', 'burger', ''],
+            ['myaccount/recipe-cms', 'pulls/4/myfeature', ''],
+            ['myaccount/recipe-cms', 'pulls/4.10/myfeature', ''],
+            ['myaccount/recipe-cms', 'pulls/burger/myfeature', ''],
+            ['myaccount/recipe-cms', '4-release', ''],
+            ['myaccount/recipe-cms', '4.10-release', ''],
+            // lockstepped repo with 4.* naming
+            ['myaccount/silverstripe-framework', '4', '4.x-dev'],
+            ['myaccount/silverstripe-framework', '4.10', '4.10.x-dev'],
+            ['myaccount/silverstripe-framework', 'burger', $latest],
+            ['myaccount/silverstripe-framework', 'pulls/4/mybugfix', '4.x-dev'],
+            ['myaccount/silverstripe-framework', 'pulls/4.10/mybugfix', '4.10.x-dev'],
+            ['myaccount/silverstripe-framework', 'pulls/burger/myfeature', $latest],
+            ['myaccount/silverstripe-framework', '4-release', '4.x-dev'],
+            ['myaccount/silverstripe-framework', '4.10-release', '4.10.x-dev'],
+            // lockstepped repo with 1.* naming
+            ['myaccount/silverstripe-admin', '1', '4.x-dev'],
+            ['myaccount/silverstripe-admin', '1.10', '4.10.x-dev'],
+            ['myaccount/silverstripe-admin', 'burger', $latest],
+            ['myaccount/silverstripe-admin', 'pulls/1/mybugfix', '4.x-dev'],
+            ['myaccount/silverstripe-admin', 'pulls/1.10/mybugfix', '4.10.x-dev'],
+            ['myaccount/silverstripe-admin', 'pulls/burger/myfeature', $latest],
+            ['myaccount/silverstripe-admin', '1-release', '4.x-dev'],
+            ['myaccount/silverstripe-admin', '1.10-release', '4.10.x-dev'],
+            // non-lockedstepped repo
+            ['myaccount/silverstripe-tagfield', '2', $latest],
+            ['myaccount/silverstripe-tagfield', '2.9', $latest],
+            ['myaccount/silverstripe-tagfield', 'burger', $latest],
+            ['myaccount/silverstripe-tagfield', 'pulls/2/mybugfix', $latest],
+            ['myaccount/silverstripe-tagfield', 'pulls/2.9/mybugfix', $latest],
+            ['myaccount/silverstripe-tagfield', 'pulls/burger/myfeature', $latest],
+            ['myaccount/silverstripe-tagfield', '2-release', $latest],
+            ['myaccount/silverstripe-tagfield', '2.9-release', $latest],
+        ];
+    }
+
+    /**
+     * @dataProvider provideGetInputsValid
+     */
+    public function testGetInputsValid(string $yml, array $expected)
+    {
+        if (!function_exists('yaml_parse')) {
+            $this->markTestSkipped('yaml extension is not installed');
+        }
+        $creator = new JobCreator();
+        $actual = $creator->getInputs($yml);
+        $this->assertSame($expected, $actual);
+    }
+
+    public function provideGetInputsValid(): array
+    {
+        return [
+            [
+                <<<EOT
+                endtoend: true
+                js: true
+                phpcoverage: false
+                phpcoverage_force_off: false
+                phplinting: true
+                phpunit: true
+                simple_matrix: false
+                github_repository: 'myaccount/silverstripe-versioned'
+                github_my_ref: 'pulls/1.10/module-standards'
+                EOT,
+                [
+                    'endtoend' => true,
+                    'js' => true,
+                    'phpcoverage' => false,
+                    'phpcoverage_force_off' => false,
+                    'phplinting' => true,
+                    'phpunit' => true,
+                    'simple_matrix' => false,
+                    'github_repository' => 'myaccount/silverstripe-versioned',
+                    'github_my_ref'=> 'pulls/1.10/module-standards'
+                ]
+            ],
+        ];
+    }
+
+     /**
+     * @dataProvider provideGetInputsInvalid
+     */
+    public function testGetInputsInvalid(string $yml, string $expectedMessage)
+    {
+        if (!function_exists('yaml_parse')) {
+            $this->markTestSkipped('yaml extension is not installed');
+        }
+        $this->expectException(Exception::class);
+        $this->expectExceptionMessage($expectedMessage);
+        $creator = new JobCreator();
+        $creator->getInputs($yml);
+    }
+
+    public function provideGetInputsInvalid(): array
+    {
+        return [
+            // missing quotes around github_my_ref (would turn into an int, so 1.10 becomes 1.1)
+            [
+                <<<EOT
+                github_my_ref: 1.10
+                EOT,
+                'github_my_ref needs to be surronded by single-quotes'
+            ],
+            // invalid yml
+            [
+                <<<EOT
+                this: --
+                    is: - total: ' nonsense
+                    "
+                EOT,
+                'Failed to parse yml'
+            ],
+        ];
+    }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..6832380
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,4 @@
+<?php
+// working directory will be root
+include 'consts.php';
+include 'job_creator.php';