-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Summary Add php detector ## How was it tested? - [x] unit tests - [ ] devbox init --auto
- Loading branch information
1 parent
8384357
commit 1f4db4a
Showing
4 changed files
with
316 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package detector | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
|
||
"go.jetpack.io/devbox/internal/searcher" | ||
) | ||
|
||
type composerJSON struct { | ||
Require map[string]string `json:"require"` | ||
} | ||
|
||
type PHPDetector struct { | ||
Root string | ||
composerJSON *composerJSON | ||
} | ||
|
||
var _ Detector = &PHPDetector{} | ||
|
||
func (d *PHPDetector) Init() error { | ||
composer, err := loadComposerJSON(d.Root) | ||
if err != nil && !os.IsNotExist(err) { | ||
return err | ||
} | ||
d.composerJSON = composer | ||
return nil | ||
} | ||
|
||
func (d *PHPDetector) Relevance(path string) (float64, error) { | ||
if d.composerJSON == nil { | ||
return 0, nil | ||
} | ||
return 1, nil | ||
} | ||
|
||
func (d *PHPDetector) Packages(ctx context.Context) ([]string, error) { | ||
packages := []string{fmt.Sprintf("php@%s", d.phpVersion(ctx))} | ||
extensions, err := d.phpExtensions(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
packages = append(packages, extensions...) | ||
return packages, nil | ||
} | ||
|
||
func (d *PHPDetector) phpVersion(ctx context.Context) string { | ||
require := d.composerJSON.Require | ||
|
||
if require["php"] == "" { | ||
return "latest" | ||
} | ||
// Remove the caret (^) if present | ||
version := strings.TrimPrefix(require["php"], "^") | ||
|
||
// Extract version in the format x, x.y, or x.y.z | ||
re := regexp.MustCompile(`^(\d+(\.\d+){0,2})`) | ||
match := re.FindString(version) | ||
|
||
return determineBestVersion(ctx, "php", match) | ||
} | ||
|
||
func (d *PHPDetector) phpExtensions(ctx context.Context) ([]string, error) { | ||
resolved, err := searcher.Client().ResolveV2(ctx, "php", d.phpVersion(ctx)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// extract major-minor from resolved.Version | ||
re := regexp.MustCompile(`^(\d+)\.(\d+)`) | ||
matches := re.FindStringSubmatch(resolved.Version) | ||
if len(matches) < 3 { | ||
return nil, fmt.Errorf("could not parse PHP version: %s", resolved.Version) | ||
} | ||
majorMinor := matches[1] + matches[2] | ||
|
||
extensions := []string{} | ||
for key := range d.composerJSON.Require { | ||
if strings.HasPrefix(key, "ext-") { | ||
// The way nix versions php extensions is inconsistent. Sometimes the version is the PHP | ||
// version, sometimes it's the extension version. We just use @latest everywhere which in | ||
// practice will just use the version of the extension that exists in the same nixpkgs as | ||
// the php version. | ||
extensions = append( | ||
extensions, | ||
fmt.Sprintf("php%sExtensions.%s@latest", majorMinor, strings.TrimPrefix(key, "ext-")), | ||
) | ||
} | ||
} | ||
|
||
return extensions, nil | ||
} | ||
|
||
func loadComposerJSON(root string) (*composerJSON, error) { | ||
composerPath := filepath.Join(root, "composer.json") | ||
composerData, err := os.ReadFile(composerPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var composer composerJSON | ||
err = json.Unmarshal(composerData, &composer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &composer, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package detector | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
"testing/fstest" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestPHPDetector_Relevance(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
fs fstest.MapFS | ||
expected float64 | ||
}{ | ||
{ | ||
name: "no composer.json", | ||
fs: fstest.MapFS{}, | ||
expected: 0, | ||
}, | ||
{ | ||
name: "with composer.json", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`{ | ||
"require": { | ||
"php": "^8.1" | ||
} | ||
}`), | ||
}, | ||
}, | ||
expected: 1, | ||
}, | ||
} | ||
|
||
for _, curTest := range tests { | ||
t.Run(curTest.name, func(t *testing.T) { | ||
dir := t.TempDir() | ||
for name, file := range curTest.fs { | ||
err := os.WriteFile(filepath.Join(dir, name), file.Data, 0o644) | ||
require.NoError(t, err) | ||
} | ||
|
||
d := &PHPDetector{Root: dir} | ||
err := d.Init() | ||
require.NoError(t, err) | ||
|
||
score, err := d.Relevance(dir) | ||
require.NoError(t, err) | ||
assert.Equal(t, curTest.expected, score) | ||
}) | ||
} | ||
} | ||
|
||
func TestPHPDetector_Packages(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
fs fstest.MapFS | ||
expectedPHP string | ||
expectedError bool | ||
}{ | ||
{ | ||
name: "no php version specified", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`{ | ||
"require": {} | ||
}`), | ||
}, | ||
}, | ||
expectedPHP: "php@latest", | ||
}, | ||
{ | ||
name: "specific php version", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`{ | ||
"require": { | ||
"php": "^8.1" | ||
} | ||
}`), | ||
}, | ||
}, | ||
expectedPHP: "[email protected]", | ||
}, | ||
{ | ||
name: "php version with patch", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`{ | ||
"require": { | ||
"php": "^8.1.2" | ||
} | ||
}`), | ||
}, | ||
}, | ||
expectedPHP: "[email protected]", | ||
}, | ||
{ | ||
name: "invalid composer.json", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`invalid json`), | ||
}, | ||
}, | ||
expectedError: true, | ||
}, | ||
} | ||
|
||
for _, curTest := range tests { | ||
t.Run(curTest.name, func(t *testing.T) { | ||
dir := t.TempDir() | ||
for name, file := range curTest.fs { | ||
err := os.WriteFile(filepath.Join(dir, name), file.Data, 0o644) | ||
require.NoError(t, err) | ||
} | ||
|
||
d := &PHPDetector{Root: dir} | ||
err := d.Init() | ||
if curTest.expectedError { | ||
require.Error(t, err) | ||
return | ||
} | ||
require.NoError(t, err) | ||
|
||
packages, err := d.Packages(context.Background()) | ||
require.NoError(t, err) | ||
assert.Equal(t, []string{curTest.expectedPHP}, packages) | ||
}) | ||
} | ||
} | ||
|
||
func TestPHPDetector_PHPExtensions(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
fs fstest.MapFS | ||
expectedExtensions []string | ||
}{ | ||
{ | ||
name: "no extensions", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`{ | ||
"require": { | ||
"php": "^8.1" | ||
} | ||
}`), | ||
}, | ||
}, | ||
expectedExtensions: []string{}, | ||
}, | ||
{ | ||
name: "multiple extensions", | ||
fs: fstest.MapFS{ | ||
"composer.json": &fstest.MapFile{ | ||
Data: []byte(`{ | ||
"require": { | ||
"php": "^8.1", | ||
"ext-mbstring": "*", | ||
"ext-imagick": "*" | ||
} | ||
}`), | ||
}, | ||
}, | ||
expectedExtensions: []string{ | ||
"php81Extensions.mbstring@latest", | ||
"php81Extensions.imagick@latest", | ||
}, | ||
}, | ||
} | ||
|
||
for _, curTest := range tests { | ||
t.Run(curTest.name, func(t *testing.T) { | ||
dir := t.TempDir() | ||
for name, file := range curTest.fs { | ||
err := os.WriteFile(filepath.Join(dir, name), file.Data, 0o644) | ||
require.NoError(t, err) | ||
} | ||
|
||
d := &PHPDetector{Root: dir} | ||
err := d.Init() | ||
require.NoError(t, err) | ||
|
||
extensions, err := d.phpExtensions(context.Background()) | ||
require.NoError(t, err) | ||
assert.ElementsMatch(t, curTest.expectedExtensions, extensions) | ||
}) | ||
} | ||
} |