mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat: use py package versions from any mise-supported version specification file (#261)
* test: add .tool-versions test * feat: extract locally defined mise package versions * test: .tool-versions unit tests * docs: integration tests docs * build: local mise config for less annoying warnings * feat: pull python ecosystem versions from mise * feat: internal support for extracting mise-specified package versions from app folder * refactor: move mise package version methods into context * test: snapshots * test: multiple test.json expectation lines this is using newly implemented multiple expectations syntax * test: support multiple expectations per test.json entry, don't use additional non-deterministic contains to throw an error * refactor: refine mise interface * refactor: make mise versions easier for a provider to use * test: snapshot update * refactor: move mise local package logic to step builder * test: move mise package version extraction to mise step builder tests * refactor: move mise package into mise step builder
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"caches": {
|
||||
"uv": {
|
||||
"directory": "/opt/uv-cache",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:latest"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise",
|
||||
".tool-versions"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/.venv"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
".venv"
|
||||
],
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "python main.py",
|
||||
"variables": {
|
||||
"PIP_DEFAULT_TIMEOUT": "100",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PYTHONFAULTHANDLER": "1",
|
||||
"PYTHONHASHSEED": "random",
|
||||
"PYTHONUNBUFFERED": "1"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"mise.toml": "[mise.toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"dest": ".tool-versions",
|
||||
"src": ".tool-versions"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "mise.toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'",
|
||||
"customName": "install mise packages: pipx, pipx:uv, python"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:latest"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_NODE_VERIFY": "false",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"uv"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"dest": "pyproject.toml",
|
||||
"src": "pyproject.toml"
|
||||
},
|
||||
{
|
||||
"dest": "uv.lock",
|
||||
"src": "uv.lock"
|
||||
},
|
||||
{
|
||||
"path": "/root/.local/bin"
|
||||
},
|
||||
{
|
||||
"path": "/app/.venv/bin"
|
||||
},
|
||||
{
|
||||
"cmd": "uv sync --locked --no-dev --no-install-project"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"PIP_DEFAULT_TIMEOUT": "100",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PYTHONFAULTHANDLER": "1",
|
||||
"PYTHONHASHSEED": "random",
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"UV_CACHE_DIR": "/opt/uv-cache",
|
||||
"UV_COMPILE_BYTECODE": "1",
|
||||
"UV_LINK_MODE": "copy",
|
||||
"UV_PYTHON_DOWNLOADS": "never",
|
||||
"VIRTUAL_ENV": "/app/.venv"
|
||||
}
|
||||
},
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "uv sync --locked --no-dev --no-editable"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -262,3 +262,14 @@ func (c *GenerateContext) NewLocalLayer() plan.Layer {
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
// in order to get around a circular dependency issue, we need to define discrete getters to interface with
|
||||
// the mise package version logic.
|
||||
|
||||
func (c *GenerateContext) GetAppSource() string {
|
||||
return c.App.Source
|
||||
}
|
||||
|
||||
func (c *GenerateContext) GetLogger() *logger.Logger {
|
||||
return c.Logger
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// build step that installs packages defined by mise configuration, provider configuration, or user configuration
|
||||
package generate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sort"
|
||||
@@ -17,6 +19,32 @@ const (
|
||||
MiseInstallCommand = "sh -c 'mise trust -a && mise install'"
|
||||
)
|
||||
|
||||
// represents a app-local mise package
|
||||
type MisePackageInfo struct {
|
||||
Version string
|
||||
Source string
|
||||
}
|
||||
|
||||
// MiseListSource represents the source of a mise tool installation
|
||||
type MiseListSource struct {
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// MiseListTool represents a tool in the mise list output
|
||||
type MiseListTool struct {
|
||||
Version string `json:"version"`
|
||||
RequestedVersion string `json:"requested_version"`
|
||||
InstallPath string `json:"install_path"`
|
||||
Source MiseListSource `json:"source"`
|
||||
Installed bool `json:"installed"`
|
||||
// --current ensures Active=true for all entries
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// MisePackageListOutput represents the full output of `mise list --current --json`
|
||||
type MisePackageListOutput map[string][]MiseListTool
|
||||
|
||||
type MiseStepBuilder struct {
|
||||
DisplayName string
|
||||
Resolver *resolver.Resolver
|
||||
@@ -84,6 +112,85 @@ func (b *MiseStepBuilder) SkipMiseInstall(name resolver.PackageRef) {
|
||||
b.Resolver.SetSkipMiseInstall(name, true)
|
||||
}
|
||||
|
||||
// GetMisePackageVersions gets all package versions from mise that are defined in the app directory environment
|
||||
// this can include additional packages defined outside the app directory, but we filter those out
|
||||
func (b *MiseStepBuilder) GetMisePackageVersions(ctx *GenerateContext) (map[string]*MisePackageInfo, error) {
|
||||
miseInstance, err := mise.New(mise.InstallDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appDir := ctx.GetAppSource()
|
||||
output, err := miseInstance.GetCurrentList(appDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get package versions: %w", err)
|
||||
}
|
||||
|
||||
var listOutput MisePackageListOutput
|
||||
if err := json.Unmarshal([]byte(output), &listOutput); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse mise list output: %w", err)
|
||||
}
|
||||
|
||||
packages := make(map[string]*MisePackageInfo)
|
||||
|
||||
for toolName, tools := range listOutput {
|
||||
var appDirTools []MiseListTool
|
||||
for _, tool := range tools {
|
||||
// Only include tools that are sourced from within the app directory
|
||||
if strings.HasPrefix(tool.Source.Path, appDir) {
|
||||
appDirTools = append(appDirTools, tool)
|
||||
}
|
||||
}
|
||||
|
||||
if len(appDirTools) > 1 {
|
||||
versions := make([]string, len(appDirTools))
|
||||
for i, tool := range appDirTools {
|
||||
versions[i] = tool.Version
|
||||
}
|
||||
|
||||
// this is possible, although in practice it should be extremely rare
|
||||
ctx.GetLogger().LogWarn("Multiple versions of tool '%s' found: %v. Using the first one: %s",
|
||||
toolName, versions, versions[0])
|
||||
}
|
||||
|
||||
if len(appDirTools) > 0 {
|
||||
firstTool := appDirTools[0]
|
||||
packages[toolName] = &MisePackageInfo{
|
||||
Version: firstTool.Version,
|
||||
// include the source so we can surface this to the user so they understand where the package version came from
|
||||
Source: firstTool.Source.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// Use mise-specified versions for all packages in the input list
|
||||
func (b *MiseStepBuilder) UseMiseVersions(ctx *GenerateContext, packages []string) {
|
||||
miseVersions, err := b.GetMisePackageVersions(ctx)
|
||||
if err != nil {
|
||||
ctx.Logger.LogWarn("Failed to get package versions from mise: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if miseVersions == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, packageName := range packages {
|
||||
if pkg := miseVersions[packageName]; pkg != nil {
|
||||
// Find the existing package reference
|
||||
for _, pkgRef := range b.MisePackages {
|
||||
if pkgRef.Name == packageName {
|
||||
b.Version(*pkgRef, pkg.Version, pkg.Source)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *MiseStepBuilder) Name() string {
|
||||
return b.DisplayName
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/railwayapp/railpack/core/resolver"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetPackageVersions(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "mise-test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ctx := CreateTestContext(t, "../../examples/python-uv-tool-versions")
|
||||
|
||||
// Create a resolver
|
||||
resolver, err := resolver.NewResolver(tempDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := &MiseStepBuilder{
|
||||
Resolver: resolver,
|
||||
app: ctx.App,
|
||||
env: ctx.Env,
|
||||
}
|
||||
|
||||
packages, err := builder.GetMisePackageVersions(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expected packages from the example
|
||||
expected := map[string]struct{}{
|
||||
"python": {},
|
||||
"uv": {},
|
||||
}
|
||||
|
||||
// Ensure ONLY expected packages are present
|
||||
require.Len(t, packages, len(expected), "unexpected number of packages returned")
|
||||
for name := range packages {
|
||||
_, ok := expected[name]
|
||||
require.True(t, ok, "unexpected package found: %s", name)
|
||||
}
|
||||
|
||||
// The python-uv-tool-versions example should have python and uv defined
|
||||
require.Contains(t, packages, "python")
|
||||
require.Contains(t, packages, "uv")
|
||||
|
||||
// Verify versions are not empty
|
||||
require.NotEmpty(t, packages["python"].Version)
|
||||
require.NotEmpty(t, packages["uv"].Version)
|
||||
|
||||
// Verify python version starts with "3.9" (as defined in .tool-versions)
|
||||
require.True(t, strings.HasPrefix(packages["python"].Version, "3.9"))
|
||||
|
||||
// Verify uv version starts with "0.7" (as defined in .tool-versions)
|
||||
require.True(t, strings.HasPrefix(packages["uv"].Version, "0.7"))
|
||||
|
||||
// Verify source types are set
|
||||
require.NotEmpty(t, packages["python"].Source)
|
||||
require.NotEmpty(t, packages["uv"].Source)
|
||||
}
|
||||
|
||||
func TestGetPackageVersionsWithNoToolVersions(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "mise-test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ctx := CreateTestContext(t, "../../examples/node-tanstack-start")
|
||||
|
||||
// Create a resolver
|
||||
resolver, err := resolver.NewResolver(tempDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := &MiseStepBuilder{
|
||||
Resolver: resolver,
|
||||
app: ctx.App,
|
||||
env: ctx.Env,
|
||||
}
|
||||
|
||||
packages, err := builder.GetMisePackageVersions(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should return empty map for directory with no .tool-versions
|
||||
require.Empty(t, packages)
|
||||
}
|
||||
+8
-2
@@ -1,3 +1,4 @@
|
||||
// helper utilities to run the mise tool
|
||||
package mise
|
||||
|
||||
import (
|
||||
@@ -44,7 +45,7 @@ func New(cacheDir string) (*Mise, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLatestVersion gets the latest version of a package matching the version constraint
|
||||
// gets the latest version of a package matching the version constraint
|
||||
func (m *Mise) GetLatestVersion(pkg, version string) (string, error) {
|
||||
_, unlock, err := m.createAndLock(pkg)
|
||||
if err != nil {
|
||||
@@ -120,6 +121,11 @@ func (m *Mise) GetAllVersions(pkg, version string) ([]string, error) {
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// GetCurrentList returns the JSON output of 'mise list --current --json' for a specific app directory
|
||||
func (m *Mise) GetCurrentList(appDir string) (string, error) {
|
||||
return m.runCmd("--cd", appDir, "list", "--current", "--json")
|
||||
}
|
||||
|
||||
// runCmd runs a mise command with the given arguments
|
||||
func (m *Mise) runCmd(args ...string) (string, error) {
|
||||
cacheDir := filepath.Join(m.cacheDir, "cache")
|
||||
@@ -180,7 +186,7 @@ func GenerateMiseToml(packages map[string]string) (string, error) {
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// createAndLock creates a file mutex and locks it, returning the mutex and an unlock function
|
||||
// lock ensuring mise does not work on the same package concurrently
|
||||
func (m *Mise) createAndLock(pkg string) (*filemutex.FileMutex, func(), error) {
|
||||
fileLockPath := filepath.Join(m.cacheDir, fmt.Sprintf("lock-%s", strings.ReplaceAll(pkg, "/", "-")))
|
||||
mu, err := filemutex.New(fileLockPath)
|
||||
|
||||
@@ -314,26 +314,36 @@ func (p *PythonProvider) InstallMisePackages(ctx *generate.GenerateContext, mise
|
||||
miseStep.Version(python, pipfileVersion, fmt.Sprintf("Pipfile > %s", pipfileVarName))
|
||||
}
|
||||
|
||||
// Collect all packages that will be used by the provider
|
||||
packages := []string{"python"}
|
||||
|
||||
// Install package managers
|
||||
if p.hasPoetry(ctx) || p.hasUv(ctx) || p.hasPdm(ctx) || p.hasPipfile(ctx) {
|
||||
miseStep.Default("pipx", "latest")
|
||||
packages = append(packages, "pipx")
|
||||
}
|
||||
|
||||
if p.hasPoetry(ctx) {
|
||||
miseStep.Default("pipx:poetry", "latest")
|
||||
packages = append(packages, "pipx:poetry")
|
||||
}
|
||||
|
||||
if p.hasPdm(ctx) {
|
||||
miseStep.Default("pipx:pdm", "latest")
|
||||
packages = append(packages, "pipx:pdm")
|
||||
}
|
||||
|
||||
if p.hasUv(ctx) {
|
||||
miseStep.Default("pipx:uv", "latest")
|
||||
packages = append(packages, "pipx:uv")
|
||||
}
|
||||
|
||||
if p.hasPipfile(ctx) {
|
||||
miseStep.Default("pipx:pipenv", "latest")
|
||||
packages = append(packages, "pipx:pipenv")
|
||||
}
|
||||
|
||||
miseStep.UseMiseVersions(ctx, packages)
|
||||
}
|
||||
|
||||
func (p *PythonProvider) GetPythonEnvVars(ctx *generate.GenerateContext) map[string]string {
|
||||
|
||||
@@ -99,6 +99,23 @@ _Note the `docker load` here to load the image into Docker. However, you can
|
||||
change the [output](https://github.com/moby/buildkit?tab=readme-ov-file#output)
|
||||
or push to a registry instead._
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Integration tests build and run example applications in containers to verify
|
||||
end-to-end functionality. Each example with a `test.json` file gets tested
|
||||
automatically.
|
||||
|
||||
```bash
|
||||
# Run all integration tests, this takes a long time. Let CI do this for you.
|
||||
mise run test-integration
|
||||
|
||||
# Run specific test
|
||||
mise run test-integration -- -run "TestExamplesIntegration/python-uv-tool-versions"
|
||||
```
|
||||
|
||||
The `test.json` file contains an array of test cases. Each case builds and runs the same
|
||||
image but checks for different expected output strings.
|
||||
|
||||
## Mise commands
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
uv 0.7.22
|
||||
python 3.9.23
|
||||
@@ -0,0 +1,11 @@
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
print("Hello from Python UV!")
|
||||
print(f"Python version: {sys.version.split()[0]}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(["uv", "--version"], capture_output=True, text=True, check=True)
|
||||
print(f"UV version: {result.stdout.strip().split()[1]}")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("UV version: not available")
|
||||
@@ -0,0 +1,7 @@
|
||||
[project]
|
||||
name = "python-specific-versions"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"expectedOutput": [
|
||||
"Hello from Python UV!",
|
||||
"Python version: 3.9.23",
|
||||
"UV version: 0.7.22"
|
||||
]
|
||||
}
|
||||
]
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "python-specific-versions"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -23,9 +24,28 @@ import (
|
||||
var buildkitCacheImport = flag.String("buildkit-cache-import", "", "BuildKit cache import configuration")
|
||||
var buildkitCacheExport = flag.String("buildkit-cache-export", "", "BuildKit cache export configuration")
|
||||
|
||||
type StringOrArray []string
|
||||
|
||||
func (s *StringOrArray) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
*s = []string{str}
|
||||
return nil
|
||||
}
|
||||
|
||||
var arr []string
|
||||
if err := json.Unmarshal(data, &arr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = arr
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
ExpectedOutput string `json:"expectedOutput"`
|
||||
Platform string `json:"platform"`
|
||||
Platform string `json:"platform"`
|
||||
// matches against the entire output of the container if it cannot be found in a single line
|
||||
ExpectedOutput StringOrArray `json:"expectedOutput"`
|
||||
Envs map[string]string `json:"envs"`
|
||||
ConfigFilePath string `json:"configFile"`
|
||||
JustBuild bool `json:"justBuild"`
|
||||
@@ -50,6 +70,7 @@ func TestExamplesIntegration(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
// read in the test assertions in each example and generate a unique test case for each entry in the JSON array
|
||||
testConfigPath := filepath.Join(examplesDir, entry.Name(), "test.json")
|
||||
if _, err := os.Stat(testConfigPath); os.IsNotExist(err) {
|
||||
continue
|
||||
@@ -127,13 +148,15 @@ func TestExamplesIntegration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// wait until cmd subprocess exits
|
||||
func cmdDoneChan(cmd *exec.Cmd) chan error {
|
||||
ch := make(chan error, 1)
|
||||
go func() { ch <- cmd.Wait() }()
|
||||
return ch
|
||||
}
|
||||
|
||||
func runContainerWithTimeout(t *testing.T, imageName, expectedOutput string, envs map[string]string, platformStr string) error {
|
||||
// this is run for each test case generated by test.json
|
||||
func runContainerWithTimeout(t *testing.T, imageName string, expectedOutputs []string, envs map[string]string, platformStr string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
@@ -177,14 +200,26 @@ func runContainerWithTimeout(t *testing.T, imageName, expectedOutput string, env
|
||||
_ = rmCmd.Run()
|
||||
}()
|
||||
|
||||
var output, errOutput strings.Builder
|
||||
// read container output and look for expected output
|
||||
var output, stdErrOutput strings.Builder
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
foundOutputs := make([]bool, len(expectedOutputs))
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
output.WriteString(line + "\n")
|
||||
if strings.Contains(line, expectedOutput) {
|
||||
|
||||
// Check if this line contains any of the expected outputs
|
||||
for i, expected := range expectedOutputs {
|
||||
if !foundOutputs[i] && strings.Contains(line, expected) {
|
||||
foundOutputs[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all expected outputs have been found
|
||||
if !slices.Contains(foundOutputs, false) {
|
||||
done <- nil
|
||||
return
|
||||
}
|
||||
@@ -193,13 +228,13 @@ func runContainerWithTimeout(t *testing.T, imageName, expectedOutput string, env
|
||||
done <- fmt.Errorf("error reading stdout: %v", err)
|
||||
return
|
||||
}
|
||||
done <- fmt.Errorf("container output:\n%s\nErrors:\n%s", output.String(), errOutput.String())
|
||||
done <- fmt.Errorf("container stdout output:\n%s\nStderr output:\n%s", output.String(), stdErrOutput.String())
|
||||
}()
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
errOutput.WriteString(scanner.Text() + "\n")
|
||||
stdErrOutput.WriteString(scanner.Text() + "\n")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -208,15 +243,15 @@ func runContainerWithTimeout(t *testing.T, imageName, expectedOutput string, env
|
||||
return fmt.Errorf("container timed out after 2 minutes")
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
require.Contains(t, output.String(), expectedOutput, "container output did not contain expected string")
|
||||
return err
|
||||
return fmt.Errorf("%v\ncontainer output did not contain expected string:\n%v", err, expectedOutputs)
|
||||
}
|
||||
// if err == nil, then we found the expected output, and can exit successfully before the container exits
|
||||
return nil
|
||||
case err := <-cmdDoneChan(cmd):
|
||||
if err != nil && !strings.Contains(err.Error(), "signal: killed") {
|
||||
return fmt.Errorf("container failed: %v", err)
|
||||
}
|
||||
require.Contains(t, output.String(), expectedOutput, "container output did not contain expected string")
|
||||
return nil
|
||||
|
||||
return fmt.Errorf("container output did not contain expected string:\n%v", expectedOutputs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user