1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-28 05:47:08 +02:00

Refactor pkg/npm and npmExecuteScripts (#1684)

This change refactors the npm pkg and npmExecuteScripts implementations
to be reusable for future steps, e.g., npmExecuteLint.

In addition, it fixes few small bugs related to unit test execution on
Windows and the fileUtils mocking implementation.

Co-authored-by: Daniel Kurzynski <daniel.kurzynski@sap.com>
Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
This commit is contained in:
Kevin Hudemann 2020-06-18 17:30:17 +02:00 committed by GitHub
parent be01dd3869
commit ceb3dd0a04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 779 additions and 343 deletions

View File

@ -421,7 +421,7 @@ func TestTriggerFortifyScan(t *testing.T) {
assert.Equal(t, []string{"install", "--user"}, runner.executions[2].parameters)
assert.Equal(t, "sourceanalyzer", runner.executions[3].executable)
assert.Equal(t, []string{"-verbose", "-64", "-b", "test", "-Xmx4G", "-Xms2G", "-python-path", "/usr/lib/python35.zip;/usr/lib/python3.5;/usr/lib/python3.5/plat-x86_64-linux-gnu;/usr/lib/python3.5/lib-dynload;/home/piper/.local/lib/python3.5/site-packages;/usr/local/lib/python3.5/dist-packages;/usr/lib/python3/dist-packages;./lib", "-exclude", "./**/tests/**/*:./**/setup.py", "./**/*"}, runner.executions[3].parameters)
assert.Equal(t, []string{"-verbose", "-64", "-b", "test", "-Xmx4G", "-Xms2G", "-python-path", "/usr/lib/python35.zip;/usr/lib/python3.5;/usr/lib/python3.5/plat-x86_64-linux-gnu;/usr/lib/python3.5/lib-dynload;/home/piper/.local/lib/python3.5/site-packages;/usr/local/lib/python3.5/dist-packages;/usr/lib/python3/dist-packages;./lib", "-exclude", fmt.Sprintf("./**/tests/**/*%s./**/setup.py", separator), "./**/*"}, runner.executions[3].parameters)
assert.Equal(t, "sourceanalyzer", runner.executions[4].executable)
assert.Equal(t, []string{"-verbose", "-64", "-b", "test", "-scan", "-Xmx4G", "-Xms2G", "-build-label", "testLabel", "-logfile", "target/fortify-scan.log", "-f", "target/result.fpr"}, runner.executions[4].parameters)

View File

@ -82,7 +82,12 @@ func mtaBuild(config mtaBuildOptions,
log.Entry().Debugf("Launching mta build")
files := piperutils.Files{}
httpClient := piperhttp.Client{}
err := runMtaBuild(config, commonPipelineEnvironment, &command.Command{}, &files, &httpClient)
e := command.Command{}
npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry, SapNpmRegistry: config.SapNpmRegistry, ExecRunner: &e}
npmExecutor := npm.NewExecutor(npmExecutorOptions)
err := runMtaBuild(config, commonPipelineEnvironment, &e, &files, &httpClient, npmExecutor)
if err != nil {
log.Entry().
WithError(err).
@ -94,7 +99,8 @@ func runMtaBuild(config mtaBuildOptions,
commonPipelineEnvironment *mtaBuildCommonPipelineEnvironment,
e execRunner,
p piperutils.FileUtils,
httpClient piperhttp.Downloader) error {
httpClient piperhttp.Downloader,
npmExecutor npm.Executor) error {
e.Stdout(log.Writer()) // not sure if using the logging framework here is a suitable approach. We handover already log formatted
e.Stderr(log.Writer()) // entries to a logging framework again. But this is considered to be some kind of project standard.
@ -106,11 +112,7 @@ func runMtaBuild(config mtaBuildOptions,
return err
}
err = npm.SetNpmRegistries(
&npm.RegistryOptions{
DefaultNpmRegistry: config.DefaultNpmRegistry,
SapNpmRegistry: config.SapNpmRegistry,
}, e)
err = npmExecutor.SetNpmRegistries()
mtaYamlFile := "mta.yaml"
mtaYamlFileExists, err := p.FileExists(mtaYamlFile)

View File

@ -1,6 +1,7 @@
package cmd
import (
"github.com/SAP/jenkins-library/pkg/npm"
"os"
"testing"
@ -25,7 +26,7 @@ func TestMarBuild(t *testing.T) {
fileUtils := MtaTestFileUtilsMock{}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.NotNil(t, err)
assert.Equal(t, "'mta.yaml' not found in project sources and 'applicationName' not provided as parameter - cannot generate 'mta.yaml' file", err.Error())
@ -43,7 +44,10 @@ func TestMarBuild(t *testing.T) {
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
npmExecutor := newNpmExecutor(&e)
npmExecutor.Options = npm.ExecutorOptions{DefaultNpmRegistry: options.DefaultNpmRegistry}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, npmExecutor)
assert.Nil(t, err)
@ -64,7 +68,10 @@ func TestMarBuild(t *testing.T) {
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
npmExecutor := newNpmExecutor(&e)
npmExecutor.Options = npm.ExecutorOptions{SapNpmRegistry: options.SapNpmRegistry}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, npmExecutor)
assert.Nil(t, err)
@ -82,7 +89,7 @@ func TestMarBuild(t *testing.T) {
fileUtils := MtaTestFileUtilsMock{}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.NotNil(t, err)
@ -101,7 +108,7 @@ func TestMarBuild(t *testing.T) {
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -140,7 +147,7 @@ func TestMarBuild(t *testing.T) {
existingFiles["mta.yaml"] = "already there"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
_ = runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Empty(t, fileUtils.writtenFiles)
})
@ -156,7 +163,7 @@ func TestMarBuild(t *testing.T) {
existingFiles["mta.yaml"] = "already there with-${timestamp}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
_ = runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.NotEmpty(t, fileUtils.writtenFiles["mta.yaml"])
})
@ -173,7 +180,7 @@ func TestMarBuild(t *testing.T) {
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -198,7 +205,7 @@ func TestMarBuild(t *testing.T) {
existingFiles["mta.yaml"] = "ID: \"myNameFromMtar\""
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -218,7 +225,7 @@ func TestMarBuild(t *testing.T) {
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -240,7 +247,7 @@ func TestMarBuild(t *testing.T) {
existingFiles["package.json"] = "{\"name\": \"myName\", \"version\": \"1.2.3\"}"
fileUtils := MtaTestFileUtilsMock{existingFiles: existingFiles}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -285,7 +292,7 @@ func TestMarBuild(t *testing.T) {
options := mtaBuildOptions{ApplicationName: "myApp", GlobalSettingsFile: "/opt/maven/settings.xml", MtaBuildTool: "cloudMbt", Platform: "CF", MtarName: "myName"}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -304,7 +311,7 @@ func TestMarBuild(t *testing.T) {
options := mtaBuildOptions{ApplicationName: "myApp", ProjectSettingsFile: "/my/project/settings.xml", MtaBuildTool: "cloudMbt", Platform: "CF", MtarName: "myName"}
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient)
err := runMtaBuild(options, &cpe, &e, &fileUtils, &httpClient, newNpmExecutor(&e))
assert.Nil(t, err)
@ -358,3 +365,9 @@ func (f *MtaTestFileUtilsMock) FileWrite(path string, content []byte, perm os.Fi
func (f *MtaTestFileUtilsMock) MkdirAll(path string, perm os.FileMode) error {
return nil
}
func newNpmExecutor(execRunner *mock.ExecMockRunner) *npm.Execute {
utils := newNpmMockUtilsBundle()
utils.execRunner = execRunner
return &npm.Execute{Utils: &utils}
}

View File

@ -6,6 +6,9 @@ import (
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/SAP/jenkins-library/pkg/nexus"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"strings"
"testing"
)
@ -48,6 +51,9 @@ func (m *mockUtilsBundle) getExecRunner() execRunner {
}
func (m *mockUtilsBundle) setProperty(pomFile, expression, value string) {
pomFile = strings.ReplaceAll(pomFile, "/", string(os.PathSeparator))
pomFile = strings.ReplaceAll(pomFile, "\\", string(os.PathSeparator))
pom := m.properties[pomFile]
if pom == nil {
pom = map[string]string{}
@ -424,7 +430,7 @@ func TestUploadMavenProjects(t *testing.T) {
utils.setProperty("pom.xml", "project.packaging", "jar")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.AddFile("pom.xml", testPomXml)
utils.AddFile("target/my-app-1.0.jar", []byte("contentsOfJar"))
utils.AddFile(filepath.Join("target", "my-app-1.0.jar"), []byte("contentsOfJar"))
uploader := mockUploader{}
options := createOptions()
@ -439,7 +445,7 @@ func TestUploadMavenProjects(t *testing.T) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "target/my-app-1.0.jar", artifacts[1].File)
assert.Equal(t, filepath.Join("target", "my-app-1.0.jar"), artifacts[1].File)
assert.Equal(t, "jar", artifacts[1].Type)
}
})
@ -451,7 +457,7 @@ func TestUploadMavenProjects(t *testing.T) {
utils.setProperty("pom.xml", "project.packaging", "<empty>")
utils.setProperty("pom.xml", "project.build.finalName", "my-app-1.0")
utils.AddFile("pom.xml", testPomXml)
utils.AddFile("target/my-app-1.0.jar", []byte("contentsOfJar"))
utils.AddFile(filepath.Join("target", "my-app-1.0.jar"), []byte("contentsOfJar"))
uploader := mockUploader{}
options := createOptions()
@ -465,7 +471,7 @@ func TestUploadMavenProjects(t *testing.T) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "target/my-app-1.0.jar", artifacts[1].File)
assert.Equal(t, filepath.Join("target", "my-app-1.0.jar"), artifacts[1].File)
assert.Equal(t, "jar", artifacts[1].Type)
}
})
@ -501,7 +507,7 @@ func TestUploadMavenProjects(t *testing.T) {
utils.setProperty("pom.xml", "project.artifactId", "my-app")
utils.setProperty("pom.xml", "project.packaging", "jar")
utils.AddFile("pom.xml", testPomXml)
utils.AddFile("target/my-app-1.0.jar", []byte("contentsOfJar"))
utils.AddFile(filepath.Join("target", "my-app-1.0.jar"), []byte("contentsOfJar"))
uploader := mockUploader{}
options := createOptions()
@ -517,7 +523,7 @@ func TestUploadMavenProjects(t *testing.T) {
if assert.Equal(t, 2, len(artifacts)) {
assert.Equal(t, "pom.xml", artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "target/my-app-1.0.jar", artifacts[1].File)
assert.Equal(t, filepath.Join("target", "my-app-1.0.jar"), artifacts[1].File)
assert.Equal(t, "jar", artifacts[1].Type)
}
})
@ -548,7 +554,7 @@ func TestUploadMavenProjects(t *testing.T) {
utils.setProperty("performance-tests/pom.xml", "project.artifactId", "my-app-app")
utils.setProperty("performance-tests/pom.xml", "project.packaging", "")
utils.AddFile("pom.xml", testPomXml)
utils.AddFile("application/pom.xml", testPomXml)
utils.AddFile(filepath.Join("application", "pom.xml"), testPomXml)
utils.AddFile("application/target/final-artifact.war", []byte("contentsOfJar"))
utils.AddFile("application/target/final-artifact-classes.jar", []byte("contentsOfClassesJar"))
utils.AddFile("integration-tests/pom.xml", testPomXml)
@ -566,13 +572,13 @@ func TestUploadMavenProjects(t *testing.T) {
artifacts := uploader.uploadedArtifacts
if assert.Equal(t, 4, len(artifacts)) {
assert.Equal(t, "application/pom.xml", artifacts[0].File)
assert.Equal(t, filepath.Join("application", "pom.xml"), artifacts[0].File)
assert.Equal(t, "pom", artifacts[0].Type)
assert.Equal(t, "application/target/final-artifact.war", artifacts[1].File)
assert.Equal(t, filepath.Join("application", "target", "final-artifact.war"), artifacts[1].File)
assert.Equal(t, "war", artifacts[1].Type)
assert.Equal(t, "application/target/final-artifact-classes.jar", artifacts[2].File)
assert.Equal(t, filepath.Join("application", "target", "final-artifact-classes.jar"), artifacts[2].File)
assert.Equal(t, "jar", artifacts[2].Type)
assert.Equal(t, "pom.xml", artifacts[3].File)
@ -585,9 +591,9 @@ func TestUploadMavenProjects(t *testing.T) {
"-DgroupId=com.mycompany.app",
"-Dversion=1.0",
"-DartifactId=my-app-app",
"-Dfile=application/pom.xml",
"-Dfile=" + filepath.Join("application", "pom.xml"),
"-Dpackaging=pom",
"-Dfiles=application/target/final-artifact.war,application/target/final-artifact-classes.jar",
"-Dfiles=" + filepath.Join("application", "target", "final-artifact.war") + "," + filepath.Join("application", "target", "final-artifact-classes.jar"),
"-Dclassifiers=,classes",
"-Dtypes=war,jar",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",

View File

@ -1,185 +1,30 @@
package cmd
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/npm"
FileUtils "github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/bmatcuk/doublestar"
"os"
"path"
"strings"
)
type npmExecuteScriptsUtilsInterface interface {
fileExists(path string) (bool, error)
glob(pattern string) (matches []string, err error)
getwd() (dir string, err error)
chdir(dir string) error
getExecRunner() execRunner
}
type npmExecuteScriptsUtilsBundle struct {
projectStructure FileUtils.ProjectStructure
fileUtils FileUtils.Files
execRunner *command.Command
}
func (u *npmExecuteScriptsUtilsBundle) fileExists(path string) (bool, error) {
return u.fileUtils.FileExists(path)
}
func (u *npmExecuteScriptsUtilsBundle) glob(pattern string) (matches []string, err error) {
return doublestar.Glob(pattern)
}
func (u *npmExecuteScriptsUtilsBundle) getwd() (dir string, err error) {
return os.Getwd()
}
func (u *npmExecuteScriptsUtilsBundle) chdir(dir string) error {
return os.Chdir(dir)
}
func (u *npmExecuteScriptsUtilsBundle) getExecRunner() execRunner {
if u.execRunner == nil {
u.execRunner = &command.Command{}
u.execRunner.Stdout(log.Writer())
u.execRunner.Stderr(log.Writer())
}
return u.execRunner
}
func npmExecuteScripts(config npmExecuteScriptsOptions, telemetryData *telemetry.CustomData) {
utils := npmExecuteScriptsUtilsBundle{}
npmExecutorOptions := npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry, SapNpmRegistry: config.SapNpmRegistry}
npmExecutor := npm.NewExecutor(npmExecutorOptions)
err := runNpmExecuteScripts(&utils, &config)
err := runNpmExecuteScripts(npmExecutor, &config)
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
func runNpmExecuteScripts(utils npmExecuteScriptsUtilsInterface, options *npmExecuteScriptsOptions) error {
execRunner := utils.getExecRunner()
packageJSONFiles, err := findPackageJSONFiles(utils)
if err != nil {
return err
}
oldWorkingDirectory, err := utils.getwd()
if err != nil {
return err
}
func runNpmExecuteScripts(npmExecutor npm.Executor, config *npmExecuteScriptsOptions) error {
packageJSONFiles := npmExecutor.FindPackageJSONFiles()
if options.VirtualFrameBuffer {
cmd, err := execRunner.RunExecutableInBackground("Xvfb", "-ac", ":99", "-screen", "0", "1280x1024x16")
if err != nil {
return fmt.Errorf("failed to start virtual frame buffer%w", err)
}
defer cmd.Kill()
execRunner.SetEnv([]string{"DISPLAY=:99"})
}
for _, file := range packageJSONFiles {
dir := path.Dir(file)
err = utils.chdir(dir)
if err != nil {
return err
}
// set in each directory to respect existing config in rc files
err = npm.SetNpmRegistries(
&npm.RegistryOptions{
DefaultNpmRegistry: options.DefaultNpmRegistry,
SapNpmRegistry: options.SapNpmRegistry,
}, execRunner)
if err != nil {
return err
}
packageLockExists, yarnLockExists, err := checkIfLockFilesExist(utils)
if err != nil {
return err
}
if options.Install {
err = installDependencies(dir, packageLockExists, yarnLockExists, execRunner)
if err != nil {
return err
}
}
for _, v := range options.RunScripts {
log.Entry().WithField("WorkingDirectory", dir).Info("run-script " + v)
err = execRunner.RunExecutable("npm", "run-script", v, "--if-present")
if err != nil {
return err
}
}
err = utils.chdir(oldWorkingDirectory)
if config.Install {
err := npmExecutor.InstallAllDependencies(packageJSONFiles)
if err != nil {
return err
}
}
return err
}
func findPackageJSONFiles(utils npmExecuteScriptsUtilsInterface) ([]string, error) {
unfilteredListOfPackageJSONFiles, err := utils.glob("**/package.json")
if err != nil {
return nil, err
}
var packageJSONFiles []string
for _, file := range unfilteredListOfPackageJSONFiles {
if strings.Contains(file, "node_modules") {
continue
}
if strings.HasPrefix(file, "gen/") || strings.Contains(file, "/gen/") {
continue
}
packageJSONFiles = append(packageJSONFiles, file)
log.Entry().Info("Discovered package.json file " + file)
}
return packageJSONFiles, nil
}
func checkIfLockFilesExist(utils npmExecuteScriptsUtilsInterface) (bool, bool, error) {
packageLockExists, err := utils.fileExists("package-lock.json")
if err != nil {
return false, false, err
}
yarnLockExists, err := utils.fileExists("yarn.lock")
if err != nil {
return false, false, err
}
return packageLockExists, yarnLockExists, nil
}
func installDependencies(dir string, packageLockExists bool, yarnLockExists bool, execRunner execRunner) (err error) {
log.Entry().WithField("WorkingDirectory", dir).Info("Running install")
if packageLockExists {
err = execRunner.RunExecutable("npm", "ci")
if err != nil {
return err
}
} else if yarnLockExists {
err = execRunner.RunExecutable("yarn", "install", "--frozen-lockfile")
if err != nil {
return err
}
} else {
log.Entry().Warn("No package lock file found. " +
"It is recommended to create a `package-lock.json` file by running `npm install` locally." +
" Add this file to your version control. " +
"By doing so, the builds of your application become more reliable.")
err = execRunner.RunExecutable("npm", "install")
if err != nil {
return err
}
}
return nil
return npmExecutor.RunScriptsInAllPackages(config.RunScripts, nil, config.VirtualFrameBuffer)
}

View File

@ -112,18 +112,18 @@ func npmExecuteScriptsMetadata() config.StepData {
{
Name: "defaultNpmRegistry",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Scope: []string{"PARAMETERS", "GENERAL", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Aliases: []config.Alias{{Name: "npm/defaultNpmRegistry"}},
},
{
Name: "sapNpmRegistry",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Scope: []string{"PARAMETERS", "GENERAL", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Aliases: []config.Alias{{Name: "npm/sapNpmRegistry"}},
},
{
Name: "virtualFrameBuffer",

View File

@ -1,161 +1,140 @@
package cmd
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/bmatcuk/doublestar"
"github.com/SAP/jenkins-library/pkg/npm"
"github.com/stretchr/testify/assert"
"sort"
"testing"
)
type npmExecuteScriptsMockUtilsBundle struct {
execRunner mock.ExecMockRunner
files map[string][]byte
type npmMockUtilsBundle struct {
*mock.FilesMock
execRunner *mock.ExecMockRunner
}
func (u *npmExecuteScriptsMockUtilsBundle) fileExists(path string) (bool, error) {
_, exists := u.files[path]
return exists, nil
func (u *npmMockUtilsBundle) GetExecRunner() npm.ExecRunner {
return u.execRunner
}
// duplicated from nexusUpload_test.go for now, refactor later?
func (u *npmExecuteScriptsMockUtilsBundle) glob(pattern string) ([]string, error) {
var matches []string
for path := range u.files {
matched, _ := doublestar.Match(pattern, path)
if matched {
matches = append(matches, path)
func newNpmMockUtilsBundle() npmMockUtilsBundle {
utils := npmMockUtilsBundle{FilesMock: &mock.FilesMock{}, execRunner: &mock.ExecMockRunner{}}
return utils
}
type npmExecutorMock struct {
utils npmMockUtilsBundle
config npmExecuteScriptsOptions
}
func (n *npmExecutorMock) FindPackageJSONFiles() []string {
packages, _ := n.utils.Glob("**/package.json")
return packages
}
func (n *npmExecutorMock) FindPackageJSONFilesWithScript(packageJSONFiles []string, script string) ([]string, error) {
return packageJSONFiles, nil
}
func (n *npmExecutorMock) RunScriptsInAllPackages(runScripts []string, runOptions []string, virtualFrameBuffer bool) error {
if len(runScripts) != len(n.config.RunScripts) {
return fmt.Errorf("RunScriptsInAllPackages was called with a different list of runScripts than config.RunScripts")
}
for i, script := range runScripts {
if script != n.config.RunScripts[i] {
return fmt.Errorf("RunScriptsInAllPackages was called with a different list of runScripts than config.RunScripts")
}
}
// The order in m.files is not deterministic, this would result in flaky tests.
sort.Strings(matches)
return matches, nil
}
func (u *npmExecuteScriptsMockUtilsBundle) getwd() (dir string, err error) {
return "/project", nil
}
if len(runOptions) != 0 {
return fmt.Errorf("RunScriptsInAllPackages was unexpectedly called with a list of runOptions")
}
if virtualFrameBuffer != n.config.VirtualFrameBuffer {
return fmt.Errorf("RunScriptsInAllPackages was called with a different value of virtualFrameBuffer than config.virtualFrameBuffer")
}
func (u *npmExecuteScriptsMockUtilsBundle) chdir(dir string) error {
return nil
}
func (u *npmExecuteScriptsMockUtilsBundle) getExecRunner() execRunner {
return &u.execRunner
func (n *npmExecutorMock) InstallAllDependencies(packageJSONFiles []string) error {
allPackages := n.FindPackageJSONFiles()
if len(packageJSONFiles) != len(allPackages) {
return fmt.Errorf("packageJSONFiles != n.FindPackageJSONFiles()")
}
for i, packageJSON := range packageJSONFiles {
if packageJSON != allPackages[i] {
return fmt.Errorf("InstallAllDependencies was called with a different list of package.json files than result of n.FindPackageJSONFiles()")
}
}
if !n.config.Install {
return fmt.Errorf("InstallAllDependencies was called but config.Install was false")
}
return nil
}
func (n *npmExecutorMock) SetNpmRegistries() error {
return nil
}
func TestNpmExecuteScripts(t *testing.T) {
t.Run("Call without install and run-scripts", func(t *testing.T) {
utils := newNpmExecuteScriptsMockUtilsBundle()
utils.files["package.json"] = []byte(`abc`)
utils.files["package-lock.json"] = []byte(`abc`)
options := npmExecuteScriptsOptions{}
t.Run("Call with install", func(t *testing.T) {
config := npmExecuteScriptsOptions{Install: true, RunScripts: []string{"ci-build", "ci-test"}}
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"name\": \"Test\" }"))
utils.AddFile("src/package.json", []byte("{\"name\": \"Test\" }"))
err := runNpmExecuteScripts(&utils, &options)
npmExecutor := npmExecutorMock{utils: utils, config: config}
err := runNpmExecuteScripts(&npmExecutor, &config)
assert.NoError(t, err)
assert.Equal(t, 2, len(utils.execRunner.Calls))
})
t.Run("Project with package lock", func(t *testing.T) {
utils := newNpmExecuteScriptsMockUtilsBundle()
utils.files["package.json"] = []byte(`abc`)
utils.files["foo/bar/node_modules/package.json"] = []byte(`abc`) // is filtered out
utils.files["gen/bar/package.json"] = []byte(`abc`) // is filtered out
utils.files["foo/gen/package.json"] = []byte(`abc`) // is filtered out
utils.files["package-lock.json"] = []byte(`abc`)
options := npmExecuteScriptsOptions{}
options.Install = true
options.RunScripts = []string{"foo", "bar"}
options.DefaultNpmRegistry = "foo.bar"
t.Run("Call without install", func(t *testing.T) {
config := npmExecuteScriptsOptions{Install: true, RunScripts: []string{"ci-build", "ci-test"}}
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"name\": \"Test\" }"))
utils.AddFile("src/package.json", []byte("{\"name\": \"Test\" }"))
err := runNpmExecuteScripts(&utils, &options)
npmExecutor := npmExecutorMock{utils: utils, config: config}
err := runNpmExecuteScripts(&npmExecutor, &config)
assert.NoError(t, err)
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "foo", "--if-present"}}, utils.execRunner.Calls[3])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "bar", "--if-present"}}, utils.execRunner.Calls[4])
assert.Equal(t, 5, len(utils.execRunner.Calls))
})
t.Run("Project with two package json files", func(t *testing.T) {
utils := newNpmExecuteScriptsMockUtilsBundle()
utils.files["package.json"] = []byte(`abc`)
utils.files["foo/bar/package.json"] = []byte(`abc`)
utils.files["package-lock.json"] = []byte(`abc`)
options := npmExecuteScriptsOptions{}
options.Install = true
options.RunScripts = []string{"foo", "bar"}
t.Run("Call with virtualFrameBuffer", func(t *testing.T) {
config := npmExecuteScriptsOptions{Install: true, RunScripts: []string{"ci-build", "ci-test"}, VirtualFrameBuffer: true}
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"name\": \"Test\" }"))
utils.AddFile("src/package.json", []byte("{\"name\": \"Test\" }"))
err := runNpmExecuteScripts(&utils, &options)
npmExecutor := npmExecutorMock{utils: utils, config: config}
err := runNpmExecuteScripts(&npmExecutor, &config)
assert.NoError(t, err)
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "foo", "--if-present"}}, utils.execRunner.Calls[3])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "bar", "--if-present"}}, utils.execRunner.Calls[4])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[7])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "foo", "--if-present"}}, utils.execRunner.Calls[8])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "bar", "--if-present"}}, utils.execRunner.Calls[9])
assert.Equal(t, 10, len(utils.execRunner.Calls))
})
t.Run("Project with yarn lock", func(t *testing.T) {
utils := newNpmExecuteScriptsMockUtilsBundle()
utils.files["package.json"] = []byte(`abc`)
utils.files["yarn.lock"] = []byte(`abc`)
options := npmExecuteScriptsOptions{}
options.Install = true
options.RunScripts = []string{"foo", "bar"}
t.Run("Test integration with npm pkg", func(t *testing.T) {
config := npmExecuteScriptsOptions{Install: true, RunScripts: []string{"ci-build"}}
err := runNpmExecuteScripts(&utils, &options)
options := npm.ExecutorOptions{SapNpmRegistry: config.SapNpmRegistry, DefaultNpmRegistry: config.DefaultNpmRegistry}
assert.NoError(t, err)
assert.Equal(t, mock.ExecCall{Exec: "yarn", Params: []string{"install", "--frozen-lockfile"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "foo", "--if-present"}}, utils.execRunner.Calls[3])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "bar", "--if-present"}}, utils.execRunner.Calls[4])
})
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-build\": \"\" } }"))
utils.AddFile("package-lock.json", []byte(""))
t.Run("Project without lock file", func(t *testing.T) {
utils := newNpmExecuteScriptsMockUtilsBundle()
utils.files["package.json"] = []byte(`abc`)
options := npmExecuteScriptsOptions{}
options.Install = true
options.RunScripts = []string{"foo", "bar"}
npmExecutor := npm.Execute{Utils: &utils, Options: options}
err := runNpmExecuteScripts(&utils, &options)
err := runNpmExecuteScripts(&npmExecutor, &config)
assert.NoError(t, err)
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"install"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "foo", "--if-present"}}, utils.execRunner.Calls[3])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "bar", "--if-present"}}, utils.execRunner.Calls[4])
})
t.Run("Call run-scripts with virtual frame buffer", func(t *testing.T) {
utils := newNpmExecuteScriptsMockUtilsBundle()
utils.files["package.json"] = []byte("{\"scripts\": { \"foo\": \"\" } }")
options := npmExecuteScriptsOptions{}
options.Install = false
options.RunScripts = []string{"foo"}
options.VirtualFrameBuffer = true
err := runNpmExecuteScripts(&utils, &options)
assert.Contains(t, utils.execRunner.Env, "DISPLAY=:99")
assert.NoError(t, err)
if assert.Len(t, utils.execRunner.Calls, 4) {
xvfbCall := utils.execRunner.Calls[0]
assert.Equal(t, "Xvfb", xvfbCall.Exec)
assert.Equal(t, []string{"-ac", ":99", "-screen", "0", "1280x1024x16"}, xvfbCall.Params)
assert.True(t, xvfbCall.Async)
assert.True(t, xvfbCall.Execution.Killed)
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run-script", "foo", "--if-present"}}, utils.execRunner.Calls[3])
if assert.NoError(t, err) {
if assert.Equal(t, 6, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"config", "get", "registry"}}, utils.execRunner.Calls[0])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"config", "get", "@sap:registry"}}, utils.execRunner.Calls[1])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run", "ci-build"}}, utils.execRunner.Calls[5])
}
}
})
}
func newNpmExecuteScriptsMockUtilsBundle() npmExecuteScriptsMockUtilsBundle {
utils := npmExecuteScriptsMockUtilsBundle{}
utils.files = map[string][]byte{}
return utils
}

View File

@ -18,6 +18,7 @@ type FilesMock struct {
files map[string]*[]byte
removedFiles map[string]*[]byte
currentDir string
Separator string
}
func (f *FilesMock) init() {
@ -27,25 +28,33 @@ func (f *FilesMock) init() {
if f.removedFiles == nil {
f.removedFiles = map[string]*[]byte{}
}
if f.Separator == "" {
f.Separator = string(os.PathSeparator)
}
}
func (f *FilesMock) toAbsPath(path string) string {
if !strings.HasPrefix(path, string(os.PathSeparator)) {
path = string(os.PathSeparator) + filepath.Join(f.currentDir, path)
if !strings.HasPrefix(path, f.Separator) {
path = f.Separator + filepath.Join(f.currentDir, path)
}
return path
}
// AddFile establishes the existence of a virtual file.
func (f *FilesMock) AddFile(path string, contents []byte) {
f.init()
f.files[f.toAbsPath(path)] = &contents
f.associateContent(path, &contents)
}
// AddDir establishes the existence of a virtual directory.
func (f *FilesMock) AddDir(path string) {
f.associateContent(path, &dirContent)
}
func (f *FilesMock) associateContent(path string, content *[]byte) {
f.init()
f.files[f.toAbsPath(path)] = &dirContent
path = strings.ReplaceAll(path, "/", f.Separator)
path = strings.ReplaceAll(path, "\\", f.Separator)
f.files[f.toAbsPath(path)] = content
}
// HasFile returns true if the virtual file system contains an entry for the given path.
@ -69,7 +78,7 @@ func (f *FilesMock) FileExists(path string) (bool, error) {
}
content, exists := f.files[f.toAbsPath(path)]
if !exists {
return false, fmt.Errorf("'%s': %w", path, os.ErrNotExist)
return false, nil
}
return content != &dirContent, nil
}
@ -81,9 +90,9 @@ func (f *FilesMock) DirExists(path string) (bool, error) {
for entry, content := range f.files {
var dirComponents []string
if content == &dirContent {
dirComponents = strings.Split(entry, string(os.PathSeparator))
dirComponents = strings.Split(entry, f.Separator)
} else {
dirComponents = strings.Split(filepath.Dir(entry), string(os.PathSeparator))
dirComponents = strings.Split(filepath.Dir(entry), f.Separator)
}
if len(dirComponents) > 0 {
dir := ""
@ -91,7 +100,7 @@ func (f *FilesMock) DirExists(path string) (bool, error) {
if i == 0 {
dir = component
} else {
dir = dir + string(os.PathSeparator) + component
dir = dir + f.Separator + component
}
if dir == path {
return true, nil
@ -169,8 +178,8 @@ func (f *FilesMock) Glob(pattern string) ([]string, error) {
return matches, nil
}
for path := range f.files {
path = strings.TrimLeft(path, string(os.PathSeparator))
matched, _ := doublestar.Match(pattern, path)
path = strings.TrimLeft(path, f.Separator)
matched, _ := doublestar.PathMatch(pattern, path)
if matched {
matches = append(matches, path)
}
@ -182,6 +191,7 @@ func (f *FilesMock) Glob(pattern string) ([]string, error) {
// Getwd returns the rooted current virtual working directory
func (f *FilesMock) Getwd() (string, error) {
f.init()
return f.toAbsPath(""), nil
}
@ -189,7 +199,7 @@ func (f *FilesMock) Getwd() (string, error) {
// The directory needs to exist according to the files and directories via AddFile() and AddDirectory().
// The implementation does not support relative path components such as "..".
func (f *FilesMock) Chdir(path string) error {
if path == "." || path == "."+string(os.PathSeparator) {
if path == "." || path == "."+f.Separator {
return nil
}
@ -200,6 +210,6 @@ func (f *FilesMock) Chdir(path string) error {
return fmt.Errorf("failed to change current directory into '%s': %w", path, os.ErrNotExist)
}
f.currentDir = strings.TrimLeft(path, string(os.PathSeparator))
f.currentDir = strings.TrimLeft(path, f.Separator)
return nil
}

View File

@ -49,7 +49,7 @@ func TestFilesMockFileExists(t *testing.T) {
err := files.Chdir("some")
assert.NoError(t, err)
exists, err := files.FileExists(path)
assert.EqualError(t, err, "'"+path+"': file does not exist")
assert.NoError(t, err)
assert.False(t, exists)
})
}

View File

@ -2,25 +2,87 @@ package npm
import (
"bytes"
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"io"
"os"
"path/filepath"
"strings"
)
// RegistryOptions holds the configured urls for npm registries
type RegistryOptions struct {
DefaultNpmRegistry string
SapNpmRegistry string
// Execute struct holds utils to enable mocking and common parameters
type Execute struct {
Utils Utils
Options ExecutorOptions
}
type execRunner interface {
// Executor interface to enable mocking for testing
type Executor interface {
FindPackageJSONFiles() []string
FindPackageJSONFilesWithScript(packageJSONFiles []string, script string) ([]string, error)
RunScriptsInAllPackages(runScripts []string, runOptions []string, virtualFrameBuffer bool) error
InstallAllDependencies(packageJSONFiles []string) error
SetNpmRegistries() error
}
// ExecutorOptions holds common parameters for functions of Executor
type ExecutorOptions struct {
DefaultNpmRegistry string
SapNpmRegistry string
ExecRunner ExecRunner
}
// NewExecutor instantiates Execute struct and sets executeOptions
func NewExecutor(executorOptions ExecutorOptions) Executor {
utils := utilsBundle{Files: &piperutils.Files{}, execRunner: executorOptions.ExecRunner}
return &Execute{
Utils: &utils,
Options: executorOptions,
}
}
// ExecRunner interface to enable mocking for testing
type ExecRunner interface {
SetEnv(e []string)
Stdout(out io.Writer)
Stderr(out io.Writer)
RunExecutable(executable string, params ...string) error
RunExecutableInBackground(executable string, params ...string) (command.Execution, error)
}
// Utils interface for mocking
type Utils interface {
Chdir(path string) error
FileExists(filename string) (bool, error)
FileRead(path string) ([]byte, error)
Getwd() (string, error)
Glob(pattern string) (matches []string, err error)
GetExecRunner() ExecRunner
}
type utilsBundle struct {
*piperutils.Files
execRunner ExecRunner
}
// GetExecRunner returns an execRunner if it's not yet initialized
func (u *utilsBundle) GetExecRunner() ExecRunner {
if u.execRunner == nil {
u.execRunner = &command.Command{}
u.execRunner.Stdout(log.Writer())
u.execRunner.Stderr(log.Writer())
}
return u.execRunner
}
// SetNpmRegistries configures the given npm registries.
// CAUTION: This will change the npm configuration in the user's home directory.
func SetNpmRegistries(options *RegistryOptions, execRunner execRunner) error {
func (exec *Execute) SetNpmRegistries() error {
execRunner := exec.Utils.GetExecRunner()
const sapRegistry = "@sap:registry"
const npmRegistry = "registry"
configurableRegistries := []string{npmRegistry, sapRegistry}
@ -38,23 +100,22 @@ func SetNpmRegistries(options *RegistryOptions, execRunner execRunner) error {
log.Entry().Info("Discovered pre-configured npm registry " + registry + " with value " + preConfiguredRegistry)
}
if registry == npmRegistry && options.DefaultNpmRegistry != "" && registryRequiresConfiguration(preConfiguredRegistry, "https://registry.npmjs.org") {
log.Entry().Info("npm registry " + registry + " was not configured, setting it to " + options.DefaultNpmRegistry)
err = execRunner.RunExecutable("npm", "config", "set", registry, options.DefaultNpmRegistry)
if registry == npmRegistry && exec.Options.DefaultNpmRegistry != "" && registryRequiresConfiguration(preConfiguredRegistry, "https://registry.npmjs.org") {
log.Entry().Info("npm registry " + registry + " was not configured, setting it to " + exec.Options.DefaultNpmRegistry)
err = execRunner.RunExecutable("npm", "config", "set", registry, exec.Options.DefaultNpmRegistry)
if err != nil {
return err
}
}
if registry == sapRegistry && options.SapNpmRegistry != "" && registryRequiresConfiguration(preConfiguredRegistry, "https://npm.sap.com") {
log.Entry().Info("npm registry " + registry + " was not configured, setting it to " + options.SapNpmRegistry)
err = execRunner.RunExecutable("npm", "config", "set", registry, options.SapNpmRegistry)
if registry == sapRegistry && exec.Options.SapNpmRegistry != "" && registryRequiresConfiguration(preConfiguredRegistry, "https://npm.sap.com") {
log.Entry().Info("npm registry " + registry + " was not configured, setting it to " + exec.Options.SapNpmRegistry)
err = execRunner.RunExecutable("npm", "config", "set", registry, exec.Options.SapNpmRegistry)
if err != nil {
return err
}
}
}
return nil
}
@ -65,3 +126,205 @@ func registryIsNonEmpty(preConfiguredRegistry string) bool {
func registryRequiresConfiguration(preConfiguredRegistry, url string) bool {
return strings.HasPrefix(preConfiguredRegistry, "undefined") || strings.HasPrefix(preConfiguredRegistry, url)
}
// RunScriptsInAllPackages runs all scripts defined in ExecuteOptions.RunScripts
func (exec *Execute) RunScriptsInAllPackages(runScripts []string, runOptions []string, virtualFrameBuffer bool) error {
packageJSONFiles := exec.FindPackageJSONFiles()
execRunner := exec.Utils.GetExecRunner()
if virtualFrameBuffer {
cmd, err := execRunner.RunExecutableInBackground("Xvfb", "-ac", ":99", "-screen", "0", "1280x1024x16")
if err != nil {
return fmt.Errorf("failed to start virtual frame buffer%w", err)
}
defer cmd.Kill()
execRunner.SetEnv([]string{"DISPLAY=:99"})
}
for _, script := range runScripts {
packagesWithScript, err := exec.FindPackageJSONFilesWithScript(packageJSONFiles, script)
if err != nil {
return err
}
if len(packagesWithScript) == 0 {
log.Entry().Warnf("could not find any package.json file with script " + script)
continue
}
for _, packageJSON := range packagesWithScript {
err = exec.executeScript(packageJSON, script, runOptions)
if err != nil {
return err
}
}
}
return nil
}
func (exec *Execute) executeScript(packageJSON string, script string, runOptions []string) error {
execRunner := exec.Utils.GetExecRunner()
oldWorkingDirectory, err := exec.Utils.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory before executing npm scripts: %w", err)
}
dir := filepath.Dir(packageJSON)
err = exec.Utils.Chdir(dir)
if err != nil {
return fmt.Errorf("failed to change into directory for executing script: %w", err)
}
// set in each directory to respect existing config in rc fileUtils
err = exec.SetNpmRegistries()
if err != nil {
return err
}
log.Entry().WithField("WorkingDirectory", dir).Info("run-script " + script)
npmRunArgs := []string{"run", script}
if len(runOptions) > 0 {
npmRunArgs = append(npmRunArgs, runOptions...)
}
err = execRunner.RunExecutable("npm", npmRunArgs...)
if err != nil {
return fmt.Errorf("failed to run npm script %s: %w", script, err)
}
err = exec.Utils.Chdir(oldWorkingDirectory)
if err != nil {
return fmt.Errorf("failed to change back into original directory: %w", err)
}
return nil
}
// FindPackageJSONFiles returns a list of all package.json fileUtils of the project excluding node_modules and gen/ directories
func (exec *Execute) FindPackageJSONFiles() []string {
unfilteredListOfPackageJSONFiles, _ := exec.Utils.Glob("**/package.json")
var packageJSONFiles []string
for _, file := range unfilteredListOfPackageJSONFiles {
if strings.Contains(file, "node_modules") {
continue
}
if strings.HasPrefix(file, "gen"+string(os.PathSeparator)) || strings.Contains(file, string(os.PathSeparator)+"gen"+string(os.PathSeparator)) {
continue
}
packageJSONFiles = append(packageJSONFiles, file)
log.Entry().Info("Discovered package.json file " + file)
}
return packageJSONFiles
}
// FindPackageJSONFilesWithScript returns a list of package.json fileUtils that contain the script
func (exec *Execute) FindPackageJSONFilesWithScript(packageJSONFiles []string, script string) ([]string, error) {
var packagesWithScript []string
for _, file := range packageJSONFiles {
var packageJSON map[string]interface{}
packageRaw, err := exec.Utils.FileRead(file)
if err != nil {
return nil, fmt.Errorf("failed to read %s to check for existence of %s script: %w", file, script, err)
}
err = json.Unmarshal(packageRaw, &packageJSON)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal %s to check for existence of %s script: %w", file, script, err)
}
scripts, ok := packageJSON["scripts"].(map[string]interface{})
if ok {
_, ok := scripts[script].(string)
if ok {
packagesWithScript = append(packagesWithScript, file)
log.Entry().Info("Discovered " + script + " script in " + file)
}
}
}
return packagesWithScript, nil
}
// InstallAllDependencies executes npm or yarn Install for all package.json fileUtils defined in packageJSONFiles
func (exec *Execute) InstallAllDependencies(packageJSONFiles []string) error {
for _, packageJSON := range packageJSONFiles {
err := exec.install(packageJSON)
if err != nil {
return err
}
}
return nil
}
// install executes npm or yarn Install for package.json
func (exec *Execute) install(packageJSON string) error {
execRunner := exec.Utils.GetExecRunner()
oldWorkingDirectory, err := exec.Utils.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory before executing npm scripts: %w", err)
}
dir := filepath.Dir(packageJSON)
err = exec.Utils.Chdir(dir)
if err != nil {
return fmt.Errorf("failed to change into directory for executing script: %w", err)
}
err = exec.SetNpmRegistries()
if err != nil {
return err
}
packageLockExists, yarnLockExists, err := exec.checkIfLockFilesExist()
if err != nil {
return err
}
log.Entry().WithField("WorkingDirectory", dir).Info("Running Install")
if packageLockExists {
err = execRunner.RunExecutable("npm", "ci")
if err != nil {
return err
}
} else if yarnLockExists {
err = execRunner.RunExecutable("yarn", "install", "--frozen-lockfile")
if err != nil {
return err
}
} else {
log.Entry().Warn("No package lock file found. " +
"It is recommended to create a `package-lock.json` file by running `npm Install` locally." +
" Add this file to your version control. " +
"By doing so, the builds of your application become more reliable.")
err = execRunner.RunExecutable("npm", "install")
if err != nil {
return err
}
}
err = exec.Utils.Chdir(oldWorkingDirectory)
if err != nil {
return fmt.Errorf("failed to change back into original directory: %w", err)
}
return nil
}
// checkIfLockFilesExist checks if yarn/package lock fileUtils exist
func (exec *Execute) checkIfLockFilesExist() (bool, bool, error) {
packageLockExists, err := exec.Utils.FileExists("package-lock.json")
if err != nil {
return false, false, err
}
yarnLockExists, err := exec.Utils.FileExists("yarn.lock")
if err != nil {
return false, false, err
}
return packageLockExists, yarnLockExists, nil
}

312
pkg/npm/npm_test.go Normal file
View File

@ -0,0 +1,312 @@
package npm
import (
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"path/filepath"
"testing"
)
type npmMockUtilsBundle struct {
*mock.FilesMock
execRunner *mock.ExecMockRunner
}
func (u *npmMockUtilsBundle) GetExecRunner() ExecRunner {
return u.execRunner
}
func newNpmMockUtilsBundle() npmMockUtilsBundle {
utils := npmMockUtilsBundle{FilesMock: &mock.FilesMock{}, execRunner: &mock.ExecMockRunner{}}
return utils
}
func TestNpm(t *testing.T) {
t.Run("find package.json files with one package.json", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"name\": \"Test\" }"))
options := ExecutorOptions{}
exec := &Execute{
Utils: &utils,
Options: options,
}
packageJSONFiles := exec.FindPackageJSONFiles()
assert.Equal(t, []string{"package.json"}, packageJSONFiles)
})
t.Run("find package.json files with two package.json and filtered package.json", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{}"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{}"))
utils.AddFile(filepath.Join("node_modules", "package.json"), []byte("{}")) // is filtered out
options := ExecutorOptions{}
exec := &Execute{
Utils: &utils,
Options: options,
}
packageJSONFiles := exec.FindPackageJSONFiles()
assert.Equal(t, []string{"package.json", filepath.Join("src", "package.json")}, packageJSONFiles)
})
t.Run("find package.json files with script", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{ \"name\": \"test\" }"))
utils.AddFile(filepath.Join("test", "package.json"), []byte("{ \"scripts\": { \"test\": \"exit 0\" } }"))
options := ExecutorOptions{}
exec := &Execute{
Utils: &utils,
Options: options,
}
packageJSONFilesWithScript, err := exec.FindPackageJSONFilesWithScript([]string{"package.json", filepath.Join("src", "package.json"), filepath.Join("test", "package.json")}, "ci-lint")
if assert.NoError(t, err) {
assert.Equal(t, []string{"package.json"}, packageJSONFilesWithScript)
}
})
t.Run("Install deps for package.json with package-lock.json", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile("package-lock.json", []byte("{}"))
options := ExecutorOptions{}
options.DefaultNpmRegistry = "foo.bar"
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.install("package.json")
if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[2])
}
}
})
t.Run("Install deps for package.json without package-lock.json", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
options := ExecutorOptions{}
options.DefaultNpmRegistry = "foo.bar"
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.install("package.json")
if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"install"}}, utils.execRunner.Calls[2])
}
}
})
t.Run("Install deps for package.json with yarn.lock", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile("yarn.lock", []byte("{}"))
options := ExecutorOptions{}
options.DefaultNpmRegistry = "foo.bar"
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.install("package.json")
if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "yarn", Params: []string{"install", "--frozen-lockfile"}}, utils.execRunner.Calls[2])
}
}
})
t.Run("Install all deps", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile("package-lock.json", []byte("{}"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile(filepath.Join("src", "package-lock.json"), []byte("{}"))
options := ExecutorOptions{}
options.DefaultNpmRegistry = "foo.bar"
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.InstallAllDependencies([]string{"package.json", filepath.Join("src", "package.json")})
if assert.NoError(t, err) {
if assert.Equal(t, 6, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"ci"}}, utils.execRunner.Calls[5])
}
}
})
t.Run("check if yarn.lock and package-lock exist", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile("yarn.lock", []byte("{}"))
utils.AddFile("package-lock.json", []byte("{}"))
options := ExecutorOptions{}
exec := &Execute{
Utils: &utils,
Options: options,
}
packageLock, yarnLock, err := exec.checkIfLockFilesExist()
if assert.NoError(t, err) {
assert.True(t, packageLock)
assert.True(t, yarnLock)
}
})
t.Run("check that yarn.lock and package-lock do not exist", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
options := ExecutorOptions{}
options.SapNpmRegistry = "foo.sap"
exec := &Execute{
Utils: &utils,
Options: options,
}
packageLock, yarnLock, err := exec.checkIfLockFilesExist()
if assert.NoError(t, err) {
assert.False(t, packageLock)
assert.False(t, yarnLock)
}
})
t.Run("check Execute script", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
options := ExecutorOptions{}
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.executeScript("package.json", "ci-lint", []string{"--silent"})
if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run", "ci-lint", "--silent"}}, utils.execRunner.Calls[2])
}
}
})
t.Run("check Execute all scripts", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{\"scripts\": { \"ci-build\": \"exit 0\" } }"))
options := ExecutorOptions{}
runScripts := []string{"ci-lint", "ci-build"}
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.RunScriptsInAllPackages(runScripts, nil, false)
if assert.NoError(t, err) {
if assert.Equal(t, 6, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run", "ci-lint"}}, utils.execRunner.Calls[2])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run", "ci-build"}}, utils.execRunner.Calls[5])
}
}
})
t.Run("check set npm registry", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{\"scripts\": { \"ci-build\": \"exit 0\" } }"))
utils.execRunner = &mock.ExecMockRunner{StdoutReturn: map[string]string{"npm config get registry": "undefined"}}
options := ExecutorOptions{}
options.DefaultNpmRegistry = "https://example.org/npm"
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.SetNpmRegistries()
if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"config", "get", "registry"}}, utils.execRunner.Calls[0])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"config", "set", "registry", exec.Options.DefaultNpmRegistry}}, utils.execRunner.Calls[1])
}
}
})
t.Run("check set npm registry", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"ci-lint\": \"exit 0\" } }"))
utils.AddFile(filepath.Join("src", "package.json"), []byte("{\"scripts\": { \"ci-build\": \"exit 0\" } }"))
utils.execRunner = &mock.ExecMockRunner{StdoutReturn: map[string]string{"npm config get @sap:registry": "undefined"}}
options := ExecutorOptions{}
options.SapNpmRegistry = "https://example.sap/npm"
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.SetNpmRegistries()
if assert.NoError(t, err) {
if assert.Equal(t, 3, len(utils.execRunner.Calls)) {
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"config", "get", "@sap:registry"}}, utils.execRunner.Calls[1])
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"config", "set", "@sap:registry", exec.Options.SapNpmRegistry}}, utils.execRunner.Calls[2])
}
}
})
t.Run("Call run-scripts with virtual frame buffer", func(t *testing.T) {
utils := newNpmMockUtilsBundle()
utils.AddFile("package.json", []byte("{\"scripts\": { \"foo\": \"\" } }"))
options := ExecutorOptions{}
exec := &Execute{
Utils: &utils,
Options: options,
}
err := exec.RunScriptsInAllPackages([]string{"foo"}, nil, true)
assert.Contains(t, utils.execRunner.Env, "DISPLAY=:99")
assert.NoError(t, err)
if assert.Len(t, utils.execRunner.Calls, 4) {
xvfbCall := utils.execRunner.Calls[0]
assert.Equal(t, "Xvfb", xvfbCall.Exec)
assert.Equal(t, []string{"-ac", ":99", "-screen", "0", "1280x1024x16"}, xvfbCall.Params)
assert.True(t, xvfbCall.Async)
assert.True(t, xvfbCall.Execution.Killed)
assert.Equal(t, mock.ExecCall{Exec: "npm", Params: []string{"run", "foo"}}, utils.execRunner.Calls[3])
}
})
}

View File

@ -28,16 +28,22 @@ spec:
description: URL of the npm registry to use. Defaults to https://registry.npmjs.org/
scope:
- PARAMETERS
- GENERAL
- STAGES
- STEPS
aliases:
- name: npm/defaultNpmRegistry
- name: sapNpmRegistry
type: string
description: The default npm registry URL to be used as the remote mirror for the SAP npm packages.
scope:
- PARAMETERS
- GENERAL
- STAGES
- STEPS
default: https://npm.sap.com
aliases:
- name: npm/sapNpmRegistry
- name: virtualFrameBuffer
type: bool
description: (Linux only) Start a virtual frame buffer in the background. This allows you to run a web browser without the need for an X server. Note that xvfb needs to be installed in the execution environment.