// Copyright 2022 Google LLC // // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package exporter import ( "bytes" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.skia.org/skia/bazel/exporter/build_proto/build" "go.skia.org/skia/bazel/exporter/interfaces/mocks" "google.golang.org/protobuf/proto" ) // The expected gn/core.gni file contents for createCoreSourcesQueryResult(). // This expected result is handmade. const publicSrcsExpectedGNI = `# DO NOT EDIT: This is a generated file. # See //bazel/exporter_tool/README.md for more information. # # The sources of truth are: # //src/core/BUILD.bazel # //src/opts/BUILD.bazel # To update this file, run make -C bazel generate_gni _src = get_path_info("../src", "abspath") # List generated by Bazel rules: # //src/core:core_srcs # //src/opts:private_hdrs skia_core_sources = [ "$_src/core/SkAAClip.cpp", "$_src/core/SkATrace.cpp", "$_src/core/SkAlphaRuns.cpp", "$_src/opts/SkBitmapProcState_opts.h", "$_src/opts/SkBlitMask_opts.h", "$_src/opts/SkBlitRow_opts.h", ] skia_core_sources += skia_pathops_sources skia_core_public += skia_pathops_public ` var exportDescs = []GNIExportDesc{ {GNI: "gn/core.gni", Vars: []GNIFileListExportDesc{ {Var: "skia_core_sources", Rules: []string{ "//src/core:core_srcs", "//src/opts:private_hdrs", }}}, }, } var testExporterParams = GNIExporterParams{ WorkspaceDir: "/path/to/workspace", ExportDescs: exportDescs, } func createCoreSourcesQueryResult() *build.QueryResult { qr := build.QueryResult{} ruleDesc := build.Target_RULE // Rule #1 srcs := []string{ "//src/core:SkAAClip.cpp", "//src/core:SkATrace.cpp", "//src/core:SkAlphaRuns.cpp", } r1 := createTestBuildRule("//src/core:core_srcs", "filegroup", "/path/to/workspace/src/core/BUILD.bazel:376:20", srcs) t1 := build.Target{Rule: r1, Type: &ruleDesc} qr.Target = append(qr.Target, &t1) // Rule #2 srcs = []string{ "//src/opts:SkBitmapProcState_opts.h", "//src/opts:SkBlitMask_opts.h", "//src/opts:SkBlitRow_opts.h", } r2 := createTestBuildRule("//src/opts:private_hdrs", "filegroup", "/path/to/workspace/src/opts/BUILD.bazel:26:10", srcs) t2 := build.Target{Rule: r2, Type: &ruleDesc} qr.Target = append(qr.Target, &t2) return &qr } func TestGNIExporterExport_ValidInput_Success(t *testing.T) { qr := createCoreSourcesQueryResult() protoData, err := proto.Marshal(qr) require.NoError(t, err) fs := mocks.NewFileSystem(t) var contents bytes.Buffer fs.On("OpenFile", mock.Anything).Once().Run(func(args mock.Arguments) { path := args.String(0) assert.True(t, filepath.IsAbs(path)) assert.Equal(t, "/path/to/workspace/gn/core.gni", filepath.ToSlash(path)) }).Return(&contents, nil).Once() e := NewGNIExporter(testExporterParams, fs) qcmd := mocks.NewQueryCommand(t) qcmd.On("Read", mock.Anything).Return(protoData, nil).Once() err = e.Export(qcmd) require.NoError(t, err) assert.Equal(t, publicSrcsExpectedGNI, contents.String()) } func TestMakeRelativeFilePathForGNI_MatchingRootDir_Success(t *testing.T) { test := func(name, target, expectedPath string) { t.Run(name, func(t *testing.T) { path, err := makeRelativeFilePathForGNI(target) require.NoError(t, err) assert.Equal(t, expectedPath, path) }) } test("src", "src/core/file.cpp", "$_src/core/file.cpp") test("include", "include/core/file.h", "$_include/core/file.h") test("modules", "modules/mod/file.cpp", "$_modules/mod/file.cpp") } func TestMakeRelativeFilePathForGNI_IndalidInput_ReturnError(t *testing.T) { test := func(name, target string) { t.Run(name, func(t *testing.T) { _, err := makeRelativeFilePathForGNI(target) assert.Error(t, err) }) } test("EmptyString", "") test("UnsupportedRootDir", "//valid/rule/incorrect/root/dir:file.cpp") } func TestIsHeaderFile_HeaderFiles_ReturnTrue(t *testing.T) { test := func(name, path string) { t.Run(name, func(t *testing.T) { assert.True(t, isHeaderFile(path)) }) } test("LowerH", "path/to/file.h") test("UpperH", "path/to/file.H") test("MixedHpp", "path/to/file.Hpp") } func TestIsHeaderFile_NonHeaderFiles_ReturnTrue(t *testing.T) { test := func(name, path string) { t.Run(name, func(t *testing.T) { assert.False(t, isHeaderFile(path)) }) } test("EmptyString", "") test("DirPath", "/path/to/dir") test("C++Source", "/path/to/file.cpp") test("DotHInDir", "/path/to/dir.h/file.cpp") test("Go", "main.go") } func TestFileListContainsOnlyCppHeaderFiles_AllHeaders_ReturnsTrue(t *testing.T) { test := func(name string, paths []string) { t.Run(name, func(t *testing.T) { assert.True(t, fileListContainsOnlyCppHeaderFiles(paths)) }) } test("OneFile", []string{"file.h"}) test("Multiple", []string{"file.h", "foo.hpp"}) } func TestFileListContainsOnlyCppHeaderFiles_NotAllHeaders_ReturnsFalse(t *testing.T) { test := func(name string, paths []string) { t.Run(name, func(t *testing.T) { assert.False(t, fileListContainsOnlyCppHeaderFiles(paths)) }) } test("Nil", nil) test("HeaderFiles", []string{"file.h", "file2.cpp"}) test("GoFile", []string{"file.go"}) } func TestWorkspaceToAbsPath_ReturnsAbsolutePath(t *testing.T) { fs := mocks.NewFileSystem(t) e := NewGNIExporter(testExporterParams, fs) require.NotNil(t, e) test := func(name, input, expected string) { t.Run(name, func(t *testing.T) { assert.Equal(t, expected, e.workspaceToAbsPath(input)) }) } test("FileInDir", "foo/bar.txt", "/path/to/workspace/foo/bar.txt") test("DirInDir", "foo/bar", "/path/to/workspace/foo/bar") test("RootFile", "root.txt", "/path/to/workspace/root.txt") test("WorkspaceDir", "", "/path/to/workspace") } func TestAbsToWorkspacePath_PathInWorkspace_ReturnsRelativePath(t *testing.T) { fs := mocks.NewFileSystem(t) e := NewGNIExporter(testExporterParams, fs) require.NotNil(t, e) test := func(name, input, expected string) { t.Run(name, func(t *testing.T) { path, err := e.absToWorkspacePath(input) assert.NoError(t, err) assert.Equal(t, expected, path) }) } test("FileInDir", "/path/to/workspace/foo/bar.txt", "foo/bar.txt") test("DirInDir", "/path/to/workspace/foo/bar", "foo/bar") test("RootFile", "/path/to/workspace/root.txt", "root.txt") test("RootFile", "/path/to/workspace/世界", "世界") test("WorkspaceDir", "/path/to/workspace", "") } func TestAbsToWorkspacePath_PathNotInWorkspace_ReturnsError(t *testing.T) { fs := mocks.NewFileSystem(t) e := NewGNIExporter(testExporterParams, fs) require.NotNil(t, e) _, err := e.absToWorkspacePath("/path/to/file.txt") assert.Error(t, err) } func TestGetGNILineVariable_LinesWithVariables_ReturnVariable(t *testing.T) { test := func(name, inputLine, expected string) { t.Run(name, func(t *testing.T) { assert.Equal(t, expected, getGNILineVariable(inputLine)) }) } test("EqualWithSpaces", `foo = [ "something" ]`, "foo") test("EqualNoSpaces", `foo=[ "something" ]`, "foo") test("EqualSpaceBefore", `foo =[ "something" ]`, "foo") test("MultilineList", `foo = [`, "foo") } func TestGetGNILineVariable_LinesWithVariables_NoMatch(t *testing.T) { test := func(name, inputLine, expected string) { t.Run(name, func(t *testing.T) { assert.Equal(t, expected, getGNILineVariable(inputLine)) }) } test("FirstCharSpace", ` foo = [ "something" ]`, "") // Impl. requires formatted file. test("NotList", `foo = bar`, "") test("ListLiteral", `[ "something" ]`, "") test("ListInComment", `# foo = [ "something" ]`, "") test("MissingVariable", `=[ "something" ]`, "") test("EmptyString", ``, "") } func TestExtractTopLevelFolder_PathsWithTopDir_ReturnsTopDir(t *testing.T) { test := func(name, input, expected string) { t.Run(name, func(t *testing.T) { assert.Equal(t, expected, extractTopLevelFolder(input)) }) } test("TopIsDir", "foo/bar/baz.txt", "foo") test("TopIsVariable", "$_src/bar/baz.txt", "$_src") test("TopIsFile", "baz.txt", "baz.txt") test("TopIsAbsDir", "/foo/bar/baz.txt", "") } func TestExtractTopLevelFolder_PathsWithNoTopDir_ReturnsEmptyString(t *testing.T) { test := func(name, input, expected string) { t.Run(name, func(t *testing.T) { assert.Equal(t, expected, extractTopLevelFolder(input)) }) } test("EmptyString", "", "") test("EmptyAbsRoot", "/", "") test("MultipleSlashes", "///", "") } func TestAddGNIVariablesToWorkspacePaths_ValidInput_ReturnsVariables(t *testing.T) { test := func(name string, inputPaths, expected []string) { t.Run(name, func(t *testing.T) { gniPaths, err := addGNIVariablesToWorkspacePaths(inputPaths) require.NoError(t, err) assert.Equal(t, expected, gniPaths) }) } test("EmptySlice", nil, []string{}) test("AllVariables", []string{"src/include/foo.h", "include/foo.h", "modules/foo.cpp"}, []string{"$_src/include/foo.h", "$_include/foo.h", "$_modules/foo.cpp"}) } func TestAddGNIVariablesToWorkspacePaths_InvalidInput_ReturnsError(t *testing.T) { test := func(name string, inputPaths []string) { t.Run(name, func(t *testing.T) { _, err := addGNIVariablesToWorkspacePaths(inputPaths) assert.Error(t, err) }) } test("InvalidTopDir", []string{"nomatch/include/foo.h"}) test("RuleNotPath", []string{"//src/core:source.cpp"}) } func TestConvertTargetsToFilePaths_ValidInput_ReturnsPaths(t *testing.T) { test := func(name string, inputTargets, expected []string) { t.Run(name, func(t *testing.T) { paths, err := convertTargetsToFilePaths(inputTargets) require.NoError(t, err) assert.Equal(t, expected, paths) }) } test("EmptySlice", nil, []string{}) test("Files", []string{"//src/include:foo.h", "//include:foo.h", "//modules:foo.cpp"}, []string{"src/include/foo.h", "include/foo.h", "modules/foo.cpp"}) } func TestConvertTargetsToFilePaths_InvalidInput_ReturnsError(t *testing.T) { test := func(name string, inputTargets []string) { t.Run(name, func(t *testing.T) { _, err := convertTargetsToFilePaths(inputTargets) assert.Error(t, err) }) } test("EmptyString", []string{""}) test("ValidTargetEmptyString", []string{"//src/include:foo.h", ""}) test("EmptyStringValidTarget", []string{"//src/include:foo.h", ""}) } func TestRemoveDuplicate_ContainsDuplicates_SortedAndDuplicatesRemoved(t *testing.T) { files := []string{ "alpha", "beta", "gamma", "delta", "beta", "Alpha", "alpha", "path/to/file", "path/to/file2", "path/to/file", } output := removeDuplicates(files) assert.Equal(t, []string{ "Alpha", "alpha", "beta", "delta", "gamma", "path/to/file", "path/to/file2", }, output) } func TestRemoveDuplicates_NoDuplicates_ReturnsOnlySorted(t *testing.T) { files := []string{ "Beta", "ALPHA", "gamma", "path/to/file2", "path/to/file", } output := removeDuplicates(files) assert.Equal(t, []string{ "ALPHA", "Beta", "gamma", "path/to/file", "path/to/file2", }, output) } func TestGetPathToTopDir_ValidRelativePaths_ReturnsExpected(t *testing.T) { test := func(name, expected, input string) { t.Run(name, func(t *testing.T) { assert.Equal(t, expected, getPathToTopDir(input)) }) } test("TopDir", ".", "core.gni") test("OneDown", "..", "gn/core.gni") test("TwoDown", "../..", "modules/skcms/skcms.gni") } func TestGetPathToTopDir_AbsolutePath_ReturnsEmptyString(t *testing.T) { // Exporter shouldn't use absolute paths, but just to be safe. assert.Equal(t, "", getPathToTopDir("/")) }