mirror of https://github.com/pulumi/pulumi.git
910 lines
27 KiB
Go
910 lines
27 KiB
Go
// Copyright 2016-2018, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/pulumi/pulumi/sdk/v3/nodejs/npm"
|
|
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestArgumentConstruction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
info := &pulumirpc.ProgramInfo{
|
|
RootDirectory: "/foo/bar",
|
|
ProgramDirectory: "/foo/bar",
|
|
EntryPoint: ".",
|
|
}
|
|
|
|
t.Run("DryRun-NoArguments", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
host := &nodeLanguageHost{}
|
|
rr := &pulumirpc.RunRequest{DryRun: true, Info: info}
|
|
args := host.constructArguments(rr, "", "", "")
|
|
assert.Contains(t, args, "--dry-run")
|
|
assert.NotContains(t, args, "true")
|
|
})
|
|
|
|
t.Run("OptionalArgs-PassedIfSpecified", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
host := &nodeLanguageHost{}
|
|
rr := &pulumirpc.RunRequest{Project: "foo", Info: info}
|
|
args := strings.Join(host.constructArguments(rr, "", "", ""), " ")
|
|
assert.Contains(t, args, "--project foo")
|
|
})
|
|
|
|
t.Run("OptionalArgs-NotPassedIfNotSpecified", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
host := &nodeLanguageHost{}
|
|
rr := &pulumirpc.RunRequest{Info: info}
|
|
args := strings.Join(host.constructArguments(rr, "", "", ""), " ")
|
|
assert.NotContains(t, args, "--stack")
|
|
})
|
|
|
|
t.Run("DotIfProgramNotSpecified", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
host := &nodeLanguageHost{}
|
|
rr := &pulumirpc.RunRequest{Info: info}
|
|
args := strings.Join(host.constructArguments(rr, "", "", ""), " ")
|
|
assert.Contains(t, args, ".")
|
|
})
|
|
|
|
t.Run("ProgramIfProgramSpecified", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
host := &nodeLanguageHost{}
|
|
rr := &pulumirpc.RunRequest{
|
|
Program: "foobar",
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: "/foo/bar",
|
|
ProgramDirectory: "/foo/bar",
|
|
EntryPoint: "foobar",
|
|
},
|
|
}
|
|
args := strings.Join(host.constructArguments(rr, "", "", ""), " ")
|
|
assert.Contains(t, args, "foobar")
|
|
})
|
|
}
|
|
|
|
func TestConfig(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("Config-Empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
host := &nodeLanguageHost{}
|
|
rr := &pulumirpc.RunRequest{Project: "foo"}
|
|
str, err := host.constructConfig(rr)
|
|
assert.NoError(t, err)
|
|
assert.JSONEq(t, "{}", str)
|
|
})
|
|
}
|
|
|
|
func TestCompatibleVersions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
a string
|
|
b string
|
|
compatible bool
|
|
errmsg string
|
|
}{
|
|
{"0.17.1", "0.16.2", false, "Differing major or minor versions are not supported."},
|
|
{"0.17.1", "1.0.0", true, ""},
|
|
{"1.0.0", "0.17.1", true, ""},
|
|
{"1.13.0", "1.13.0", true, ""},
|
|
{"1.1.1", "1.13.0", true, ""},
|
|
{"1.13.0", "1.1.1", true, ""},
|
|
{"1.1.0", "2.1.0", true, ""},
|
|
{"2.1.0", "1.1.0", true, ""},
|
|
{"1.1.0", "2.0.0-beta1", true, ""},
|
|
{"2.0.0-beta1", "1.1.0", true, ""},
|
|
{"2.1.0", "3.1.0", false, "Differing major versions are not supported."},
|
|
{"0.16.1", "1.0.0", false, "Differing major or minor versions are not supported."},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
compatible, errmsg := compatibleVersions(semver.MustParse(c.a), semver.MustParse(c.b))
|
|
assert.Equal(t, c.errmsg, errmsg)
|
|
assert.Equal(t, c.compatible, compatible)
|
|
}
|
|
}
|
|
|
|
func TestGetRequiredPlugins(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
|
|
files := []struct {
|
|
path string
|
|
content string
|
|
}{
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "foo", "package.json"),
|
|
`{ "name": "@pulumi/foo", "version": "1.2.3", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "bar", "package.json"),
|
|
`{ "name": "@pulumi/bar", "version": "4.5.6", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "baz", "package.json"),
|
|
`{ "name": "@pulumi/baz", "version": "4.5.6", "pulumi": { "resource": false } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "malformed", "tests", "malformed_test", "package.json"),
|
|
`{`,
|
|
},
|
|
}
|
|
for _, file := range files {
|
|
err := os.MkdirAll(filepath.Dir(file.path), 0o755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(file.path, []byte(file.content), 0o600)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetRequiredPlugins(context.Background(), &pulumirpc.GetRequiredPluginsRequest{
|
|
Program: dir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: dir,
|
|
ProgramDirectory: dir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
actual := make(map[string]string)
|
|
for _, plugin := range resp.GetPlugins() {
|
|
actual[plugin.Name] = plugin.Version
|
|
}
|
|
assert.Equal(t, map[string]string{
|
|
"foo": "v1.2.3",
|
|
"bar": "v4.5.6",
|
|
}, actual)
|
|
}
|
|
|
|
func TestGetRequiredPluginsSymlinkCycles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
|
|
files := []struct {
|
|
path string
|
|
content string
|
|
}{
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "foo", "package.json"),
|
|
`{ "name": "@pulumi/foo", "version": "1.2.3", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "bar", "package.json"),
|
|
`{ "name": "@pulumi/bar", "version": "4.5.6", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "baz", "package.json"),
|
|
`{ "name": "@pulumi/baz", "version": "4.5.6", "pulumi": { "resource": false } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "malformed", "tests", "malformed_test", "package.json"),
|
|
`{`,
|
|
},
|
|
}
|
|
for _, file := range files {
|
|
err := os.MkdirAll(filepath.Dir(file.path), 0o755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(file.path, []byte(file.content), 0o600)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Add a symlink cycle in
|
|
err := os.Symlink(filepath.Join(dir, "node_modules"), filepath.Join(dir, "node_modules", "@node_modules"))
|
|
require.NoError(t, err)
|
|
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetRequiredPlugins(context.Background(), &pulumirpc.GetRequiredPluginsRequest{
|
|
Program: dir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: dir,
|
|
ProgramDirectory: dir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
actual := make(map[string]string)
|
|
for _, plugin := range resp.GetPlugins() {
|
|
actual[plugin.Name] = plugin.Version
|
|
}
|
|
assert.Equal(t, map[string]string{
|
|
"foo": "v1.2.3",
|
|
"bar": "v4.5.6",
|
|
}, actual)
|
|
}
|
|
|
|
func TestGetRequiredPluginsSymlinkCycles2(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := filepath.Join(t.TempDir(), "testdir")
|
|
err := os.Mkdir(dir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
files := []struct {
|
|
path string
|
|
content string
|
|
}{
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "foo", "package.json"),
|
|
`{ "name": "@pulumi/foo", "version": "1.2.3", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "bar", "package.json"),
|
|
`{ "name": "@pulumi/bar", "version": "4.5.6", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "baz", "package.json"),
|
|
`{ "name": "@pulumi/baz", "version": "4.5.6", "pulumi": { "resource": false } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "malformed", "tests", "malformed_test", "package.json"),
|
|
`{`,
|
|
},
|
|
}
|
|
for _, file := range files {
|
|
err := os.MkdirAll(filepath.Dir(file.path), 0o755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(file.path, []byte(file.content), 0o600)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Add a symlink cycle in
|
|
err = os.Symlink(filepath.Join("..", ".."), filepath.Join(dir, "node_modules", "@node_modules"))
|
|
require.NoError(t, err)
|
|
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetRequiredPlugins(context.Background(), &pulumirpc.GetRequiredPluginsRequest{
|
|
Program: dir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: dir,
|
|
ProgramDirectory: dir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
actual := make(map[string]string)
|
|
for _, plugin := range resp.GetPlugins() {
|
|
actual[plugin.Name] = plugin.Version
|
|
}
|
|
assert.Equal(t, map[string]string{
|
|
"foo": "v1.2.3",
|
|
"bar": "v4.5.6",
|
|
}, actual)
|
|
}
|
|
|
|
func TestGetRequiredPluginsNestedPolicyPack(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := filepath.Join(t.TempDir(), "testdir")
|
|
err := os.Mkdir(dir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
files := []struct {
|
|
path string
|
|
content string
|
|
}{
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "foo", "package.json"),
|
|
`{ "name": "@pulumi/foo", "version": "1.2.3", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "node_modules", "@pulumi", "bar", "package.json"),
|
|
`{ "name": "@pulumi/bar", "version": "4.5.6", "pulumi": { "resource": true } }`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "policy", "PulumiPolicy.yaml"),
|
|
`name: my-policy`,
|
|
},
|
|
{
|
|
filepath.Join(dir, "policy", "node_modules", "@pulumi", "baz", "package.json"),
|
|
`{ "name": "@pulumi/baz", "version": "7.8.9", "pulumi": { "resource": true } }`,
|
|
},
|
|
}
|
|
for _, file := range files {
|
|
err := os.MkdirAll(filepath.Dir(file.path), 0o755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(file.path, []byte(file.content), 0o600)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetRequiredPlugins(context.Background(), &pulumirpc.GetRequiredPluginsRequest{
|
|
Program: dir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: dir,
|
|
ProgramDirectory: dir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
actual := make(map[string]string)
|
|
for _, plugin := range resp.GetPlugins() {
|
|
actual[plugin.Name] = plugin.Version
|
|
}
|
|
assert.Equal(t, map[string]string{
|
|
"foo": "v1.2.3",
|
|
"bar": "v4.5.6",
|
|
// baz: v7.8.9 is not included because it is in a nested policy pack
|
|
}, actual)
|
|
}
|
|
|
|
type filePathAndContents struct {
|
|
path string
|
|
content string
|
|
}
|
|
|
|
func setupFiles(t *testing.T, files []filePathAndContents) string {
|
|
dir := filepath.Join(t.TempDir(), "program-dependency-testdir")
|
|
err := os.Mkdir(dir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
for _, file := range files {
|
|
file.path = filepath.Join(dir, file.path)
|
|
err := os.MkdirAll(filepath.Dir(file.path), 0o755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(file.path, []byte(file.content), 0o600)
|
|
require.NoError(t, err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func TestGetProgramDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("With no package.json, no lock files", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "Pulumi.yaml",
|
|
content: `name: test`,
|
|
},
|
|
})
|
|
host := &nodeLanguageHost{}
|
|
_, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: testDir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: testDir,
|
|
ProgramDirectory: testDir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.ErrorContains(t, err, "no package-lock.json or yarn.lock file found (searching upwards from")
|
|
})
|
|
|
|
t.Run("With package.json in project root, no lock files", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "package.json",
|
|
content: `{ "name": "@pulumi/baz", "dependencies": { "@pulumi/pulumi": "^3.113.0" } }`,
|
|
},
|
|
{
|
|
path: "Pulumi.yaml",
|
|
content: `name: test`,
|
|
},
|
|
})
|
|
host := &nodeLanguageHost{}
|
|
_, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: testDir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: testDir,
|
|
ProgramDirectory: testDir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.ErrorContains(t, err, "no package-lock.json or yarn.lock file found (searching upwards from")
|
|
})
|
|
|
|
t.Run("With package.json and yarn.lock in project root", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "package.json",
|
|
content: `{ "name": "@pulumi/baz", "dependencies": { "@pulumi/pulumi": "^3.113.0" } }`,
|
|
},
|
|
{
|
|
path: "Pulumi.yaml",
|
|
content: `name: test`,
|
|
},
|
|
{
|
|
path: "yarn.lock",
|
|
content: `"@pulumi/pulumi@^3.0.0", "@pulumi/pulumi@^3.113.0":
|
|
version "3.131.0"
|
|
resolved "https://registry.yarnpkg.com/@pulumi/pulumi/-/pulumi-3.131.0.tgz#6233e5ee5e72907b99415b32be6a9ebf9041f096"
|
|
integrity sha512-QNtQeav3dkU0mRdMe2TVvkBmIGkBevVvbD7/bt0fJlGoX/onzv5tysqi1GWCkXsq0FKtBtGYNpVD6wH0cqMN6g==
|
|
dependencies:
|
|
"@grpc/grpc-js" "^1.10.1"
|
|
"@logdna/tail-file" "^2.0.6"
|
|
"@npmcli/arborist" "^7.3.1"`,
|
|
},
|
|
})
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: testDir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: testDir,
|
|
ProgramDirectory: testDir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(resp.Dependencies), 1)
|
|
require.Equal(t, resp.Dependencies[0].Name, "@pulumi/pulumi")
|
|
require.Equal(t, resp.Dependencies[0].Version, "3.131.0")
|
|
})
|
|
|
|
t.Run("With package.json and yarn.lock in parent dir", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "package.json",
|
|
content: `{ "name": "@pulumi/baz", "dependencies": { "@pulumi/pulumi": "^3.113.0" } }`,
|
|
},
|
|
{
|
|
path: filepath.Join("subdir", "Pulumi.yaml"),
|
|
content: `name: test`,
|
|
},
|
|
{
|
|
path: "yarn.lock",
|
|
content: `"@pulumi/pulumi@^3.0.0", "@pulumi/pulumi@^3.113.0":
|
|
version "3.131.0"
|
|
resolved "https://registry.yarnpkg.com/@pulumi/pulumi/-/pulumi-3.131.0.tgz#6233e5ee5e72907b99415b32be6a9ebf9041f096"
|
|
integrity sha512-QNtQeav3dkU0mRdMe2TVvkBmIGkBevVvbD7/bt0fJlGoX/onzv5tysqi1GWCkXsq0FKtBtGYNpVD6wH0cqMN6g==
|
|
dependencies:
|
|
"@grpc/grpc-js" "^1.10.1"
|
|
"@logdna/tail-file" "^2.0.6"
|
|
"@npmcli/arborist" "^7.3.1"`,
|
|
},
|
|
})
|
|
subdir := filepath.Join(testDir, "subdir")
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: subdir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: subdir,
|
|
ProgramDirectory: subdir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(resp.Dependencies), 1)
|
|
require.Equal(t, resp.Dependencies[0].Name, "@pulumi/pulumi")
|
|
require.Equal(t, resp.Dependencies[0].Version, "3.131.0")
|
|
})
|
|
|
|
t.Run("With package.json and yarn.lock in project root", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "package.json",
|
|
content: `{ "name": "@pulumi/baz", "dependencies": { "@pulumi/pulumi": "^3.113.0" } }`,
|
|
},
|
|
{
|
|
path: "Pulumi.yaml",
|
|
content: `name: test`,
|
|
},
|
|
{
|
|
path: "yarn.lock",
|
|
content: `"@pulumi/pulumi@^3.0.0", "@pulumi/pulumi@^3.113.0":
|
|
version "3.131.0"
|
|
resolved "https://registry.yarnpkg.com/@pulumi/pulumi/-/pulumi-3.131.0.tgz#6233e5ee5e72907b99415b32be6a9ebf9041f096"
|
|
integrity sha512-QNtQeav3dkU0mRdMe2TVvkBmIGkBevVvbD7/bt0fJlGoX/onzv5tysqi1GWCkXsq0FKtBtGYNpVD6wH0cqMN6g==
|
|
dependencies:
|
|
"@grpc/grpc-js" "^1.10.1"
|
|
"@logdna/tail-file" "^2.0.6"
|
|
"@npmcli/arborist" "^7.3.1"`,
|
|
},
|
|
})
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: testDir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: testDir,
|
|
ProgramDirectory: testDir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(resp.Dependencies), 1)
|
|
require.Equal(t, resp.Dependencies[0].Name, "@pulumi/pulumi")
|
|
require.Equal(t, resp.Dependencies[0].Version, "3.131.0")
|
|
})
|
|
|
|
t.Run("With package.json and package-lock.json in project root", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "package.json",
|
|
content: `{ "dependencies": { "random": "^5.1.0" } }`,
|
|
},
|
|
{
|
|
path: "Pulumi.yaml",
|
|
content: `name: test`,
|
|
},
|
|
{
|
|
path: "package-lock.json",
|
|
content: `{
|
|
"name": "pulumi-repos",
|
|
"lockfileVersion": 3,
|
|
"requires": true,
|
|
"packages": {
|
|
"": {
|
|
"dependencies": {
|
|
"random": "^5.1.0"
|
|
}
|
|
},
|
|
"node_modules/random": {
|
|
"version": "5.1.0",
|
|
"resolved": "https://registry.npmjs.org/random/-/random-5.1.0.tgz",
|
|
"integrity": "sha512-0NGG4HMW9sTstLbignEDasSQJlCGkNQZICIWStZ+h4SzSJfZXpecGKV7qL0AOKcIT8XX9pJ49uZnvI0n/Y+vWA==",
|
|
"engines": {
|
|
"node": ">=18"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
path: filepath.Join("node_modules", "random", "package.json"),
|
|
content: `{ "name": "random", "version": "5.1.0", "type": "module" }`,
|
|
},
|
|
})
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: testDir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: testDir,
|
|
ProgramDirectory: testDir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(resp.Dependencies))
|
|
require.Equal(t, "random", resp.Dependencies[0].Name)
|
|
require.Equal(t, "5.1.0", resp.Dependencies[0].Version)
|
|
})
|
|
|
|
t.Run("With package.json and package-lock.json in parent dir", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testDir := setupFiles(t, []filePathAndContents{
|
|
{
|
|
path: "package.json",
|
|
content: `{ "dependencies": { "random": "^5.1.0" } }`,
|
|
},
|
|
{
|
|
path: filepath.Join("subdir", "Pulumi.yaml"),
|
|
content: `name: test`,
|
|
},
|
|
{
|
|
path: "package-lock.json",
|
|
content: `{
|
|
"name": "pulumi-repos",
|
|
"lockfileVersion": 3,
|
|
"requires": true,
|
|
"packages": {
|
|
"": {
|
|
"dependencies": {
|
|
"random": "^5.1.0"
|
|
}
|
|
},
|
|
"node_modules/random": {
|
|
"version": "5.1.0",
|
|
"resolved": "https://registry.npmjs.org/random/-/random-5.1.0.tgz",
|
|
"integrity": "sha512-0NGG4HMW9sTstLbignEDasSQJlCGkNQZICIWStZ+h4SzSJfZXpecGKV7qL0AOKcIT8XX9pJ49uZnvI0n/Y+vWA==",
|
|
"engines": {
|
|
"node": ">=18"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
path: filepath.Join("node_modules", "random", "package.json"),
|
|
content: `{ "name": "random", "version": "5.1.0", "type": "module" }`,
|
|
},
|
|
})
|
|
|
|
subdir := filepath.Join(testDir, "subdir")
|
|
host := &nodeLanguageHost{}
|
|
resp, err := host.GetProgramDependencies(context.Background(), &pulumirpc.GetProgramDependenciesRequest{
|
|
Program: subdir,
|
|
Info: &pulumirpc.ProgramInfo{
|
|
RootDirectory: subdir,
|
|
ProgramDirectory: subdir,
|
|
EntryPoint: ".",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(resp.Dependencies))
|
|
require.Equal(t, "random", resp.Dependencies[0].Name)
|
|
require.Equal(t, "5.1.0", resp.Dependencies[0].Version)
|
|
})
|
|
}
|
|
|
|
func TestParseOptions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts, err := parseOptions(nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, npm.AutoPackageManager, opts.packagemanager)
|
|
|
|
_, err = parseOptions(map[string]interface{}{
|
|
"typescript": 123,
|
|
})
|
|
require.ErrorContains(t, err, "typescript option must be a boolean")
|
|
|
|
_, err = parseOptions(map[string]interface{}{
|
|
"packagemanager": "poetry",
|
|
})
|
|
require.ErrorContains(t, err, "packagemanager option must be one of")
|
|
|
|
for _, tt := range []struct {
|
|
input string
|
|
expected npm.PackageManagerType
|
|
}{
|
|
{"auto", npm.AutoPackageManager},
|
|
{"npm", npm.NpmPackageManager},
|
|
{"yarn", npm.YarnPackageManager},
|
|
{"pnpm", npm.PnpmPackageManager},
|
|
} {
|
|
opts, err = parseOptions(map[string]interface{}{
|
|
"packagemanager": tt.input,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expected, opts.packagemanager)
|
|
}
|
|
}
|
|
|
|
// Nodejs sometimes sets stdout/stderr to non-blocking mode. When a nodejs subprocess is directly
|
|
// handed the go process's stdout/stderr file descriptors, nodejs's non-blocking configuration goes
|
|
// unnoticed by go, and a write from go can result in an error `write /dev/stdout: resource
|
|
// temporarily unavailable`. See runWithOutput for more details.
|
|
func TestNonblockingStdout(t *testing.T) {
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/16503
|
|
t.Parallel()
|
|
|
|
script := `import os, time
|
|
os.set_blocking(1, False) # set stdout to non-blocking
|
|
time.sleep(3)
|
|
`
|
|
|
|
// Create a named pipe to use as stdout
|
|
tmp := os.TempDir()
|
|
p := filepath.Join(tmp, "fake-stdout")
|
|
err := syscall.Mkfifo(p, 0o644)
|
|
defer os.Remove(p)
|
|
require.NoError(t, err)
|
|
// Open fd without O_NONBLOCK, ensuring that os.NewFile does not return a pollable file.
|
|
// When our python script changes the file to non-blocking, Go does not notice and continues to
|
|
// expect the file to be blocking, and we can trigger the bug.
|
|
fd, err := syscall.Open(p, syscall.O_CREAT|syscall.O_RDWR, 0o644)
|
|
require.NoError(t, err)
|
|
fakeStdout := os.NewFile(uintptr(fd), p)
|
|
defer fakeStdout.Close()
|
|
require.NotNil(t, fakeStdout)
|
|
|
|
cmd := exec.Command("python3", "-c", script)
|
|
|
|
var done bool
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
for !done {
|
|
s := "....................\n"
|
|
n, err := fakeStdout.Write([]byte(s))
|
|
require.NoError(t, err)
|
|
require.Equal(t, n, len(s))
|
|
}
|
|
}()
|
|
|
|
require.NoError(t, runWithOutput(cmd, fakeStdout, os.Stderr))
|
|
done = true
|
|
}
|
|
|
|
type slowWriter struct {
|
|
nWrites *int
|
|
}
|
|
|
|
func (s slowWriter) Write(b []byte) (int, error) {
|
|
time.Sleep(100 * time.Millisecond)
|
|
l := len(b)
|
|
*s.nWrites += l
|
|
return l, nil
|
|
}
|
|
|
|
func TestRunWithOutputDoesNotMissData(t *testing.T) {
|
|
// This test ensures that runWithOutput writes all the data from the command and does not miss
|
|
// any data that might be buffered when the command exits.
|
|
t.Parallel()
|
|
|
|
// Write `o` to stdout 100 times at 10 ms interval, followed by `x\n`
|
|
// Write `e` to stderr 100 times at 10 ms interval, followed by `x\n`
|
|
script := `let i = 0;
|
|
let interval = setInterval(() => {
|
|
process.stdout.write("o");
|
|
process.stderr.write("e");
|
|
i++;
|
|
if (i == 100) {
|
|
process.stdout.write("x\n");
|
|
process.stderr.write("x\n");
|
|
clearInterval(interval);
|
|
}
|
|
}, 10)
|
|
`
|
|
|
|
cmd := exec.Command("node", "-e", script)
|
|
stdout := slowWriter{nWrites: new(int)}
|
|
stderr := slowWriter{nWrites: new(int)}
|
|
|
|
require.NoError(t, runWithOutput(cmd, stdout, stderr))
|
|
|
|
require.Equal(t, 100+2 /* "x\n" */, *stdout.nWrites)
|
|
require.Equal(t, 100+2 /* "x\n" */, *stderr.nWrites)
|
|
}
|
|
|
|
//nolint:paralleltest // mutates environment variables
|
|
func TestUseFnm(t *testing.T) {
|
|
// Set $PATH to to $TMPDIR/bin so that no `fnm` executable can be found.
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "bin"), 0o755))
|
|
t.Setenv("PATH", filepath.Join(tmpDir, "bin"))
|
|
|
|
_, err := useFnm(tmpDir)
|
|
require.ErrorIs(t, err, errFnmNotFound)
|
|
|
|
// Add a fake fnm binary to $TMPDIR/bin for the rest of the tests.
|
|
//nolint:gosec // we want this file to be executable
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "bin", "fnm"), []byte("#!/bin/sh\nexit 0;\n"), 0o700))
|
|
|
|
t.Run("no version files", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
_, err := useFnm(tmpDir)
|
|
require.ErrorIs(t, err, errVersionFileNotFound)
|
|
})
|
|
|
|
t.Run(".node-version in cwd", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".node-version"), []byte("22.7.3"), 0o600))
|
|
version, err := useFnm(tmpDir)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "22.7.3", version)
|
|
})
|
|
|
|
t.Run(".nvmrc in cwd", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".nvmrc"), []byte("20.1.1"), 0o600))
|
|
version, err := useFnm(tmpDir)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "20.1.1", version)
|
|
})
|
|
|
|
t.Run(".nvmrc & .node-version in cwd", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
// .nvmrc should take precedence
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".nvmrc"), []byte("20.1.1"), 0o600))
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".node-version"), []byte("22.7.3"), 0o600))
|
|
version, err := useFnm(tmpDir)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "20.1.1", version)
|
|
})
|
|
|
|
t.Run(".node-version in parent folder", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
tmpDirNested := filepath.Join(tmpDir, "nested")
|
|
require.NoError(t, os.MkdirAll(tmpDirNested, 0o700))
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".node-version"), []byte("20.1.1"), 0o600))
|
|
version, err := useFnm(tmpDirNested)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "20.1.1", version)
|
|
})
|
|
|
|
t.Run(".node-version in cwd & parent", func(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
tmpDirNested := filepath.Join(tmpDir, "nested")
|
|
require.NoError(t, os.MkdirAll(tmpDirNested, 0o700))
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".node-version"), []byte("20.1.1"), 0o600))
|
|
// This should take precedence over the parent folder's .node-version
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDirNested, ".node-version"), []byte("20.7.3"), 0o600))
|
|
version, err := useFnm(tmpDirNested)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "20.7.3", version)
|
|
})
|
|
}
|
|
|
|
//nolint:paralleltest // mutates environment variables
|
|
func TestNodeInstall(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip()
|
|
}
|
|
tmpDir := t.TempDir()
|
|
t.Setenv("PATH", filepath.Join(tmpDir, "bin"))
|
|
|
|
fmt.Println(os.Getenv("PATH"))
|
|
|
|
// There's no fnm executable in PATH, installNodeVersion is a no-op
|
|
stdout := &bytes.Buffer{}
|
|
err := installNodeVersion(tmpDir, stdout)
|
|
require.ErrorIs(t, err, errFnmNotFound)
|
|
|
|
// Add a mock fnm executable to $tmp/bin. For each execution, the mock fnm executable will
|
|
// append a line with its arguments to a file. We read back the file to verify that it was
|
|
// called with the expected arguments.
|
|
require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "bin"), 0o755))
|
|
outPath := filepath.Join(tmpDir, "out.txt")
|
|
script := fmt.Sprintf("#!/bin/sh\necho $@ >> %s\n", outPath)
|
|
//nolint:gosec // we want this file to be executable
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "bin", "fnm"), []byte(script), 0o700))
|
|
|
|
// There's no .node-version or .nvmrc file, so the binary should not be called.
|
|
// We expect the file written by our mock fnm executable to not exist.
|
|
stdout = &bytes.Buffer{}
|
|
err = installNodeVersion(tmpDir, stdout)
|
|
require.Error(t, err, errVersionFileNotFound)
|
|
|
|
// Create a .node-version file
|
|
// The mock fnm executable should be called with a command to install the requested version,
|
|
// and a command to set the default version to this version.
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".node-version"), []byte("20.1.2"), 0o600))
|
|
stdout = &bytes.Buffer{}
|
|
err = installNodeVersion(tmpDir, stdout)
|
|
require.NoError(t, err)
|
|
b, err := os.ReadFile(outPath)
|
|
require.NoError(t, err)
|
|
commands := strings.Split(strings.TrimSpace(string(b)), "\n")
|
|
require.Equal(t, "install 20.1.2 --progress never", commands[0])
|
|
require.Equal(t, "alias 20.1.2 default", commands[1])
|
|
}
|