diff --git a/internal/lookup/library.go b/internal/lookup/library.go index 7bb62f68..73ad6984 100644 --- a/internal/lookup/library.go +++ b/internal/lookup/library.go @@ -66,7 +66,7 @@ func newLdcacheLocator(logger logger.Interface, root string) Locator { return nil } - return ldcacheLocator{ + return &ldcacheLocator{ logger: logger, cache: cache, } @@ -82,7 +82,7 @@ func (l ldcacheLocator) Locate(libname string) ([]string, error) { } if len(paths64) == 0 { - return nil, fmt.Errorf("64-bit library %v not found", libname) + return nil, fmt.Errorf("64-bit library %v: %w", libname, errNotFound) } return paths64, nil diff --git a/internal/lookup/library_test.go b/internal/lookup/library_test.go new file mode 100644 index 00000000..5682281d --- /dev/null +++ b/internal/lookup/library_test.go @@ -0,0 +1,190 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 lookup + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/NVIDIA/nvidia-container-toolkit/internal/ldcache" + testlog "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" +) + +func TestLDCacheLocator(t *testing.T) { + logger, _ := testlog.NewNullLogger() + + testDir := t.TempDir() + symlinkDir := filepath.Join(testDir, "/lib/symlink") + require.NoError(t, os.MkdirAll(symlinkDir, 0755)) + + versionLib := filepath.Join(symlinkDir, "libcuda.so.1.2.3") + soLink := filepath.Join(symlinkDir, "libcuda.so") + sonameLink := filepath.Join(symlinkDir, "libcuda.so.1") + + _, err := os.Create(versionLib) + require.NoError(t, err) + require.NoError(t, os.Symlink(versionLib, sonameLink)) + require.NoError(t, os.Symlink(sonameLink, soLink)) + + lut := newLdcacheLocator(logger, testDir) + + testCases := []struct { + description string + libname string + ldcacheMap map[string]string + expected []string + expectedError error + }{ + { + description: "lib only resolves in LDCache", + libname: "libcuda.so", + ldcacheMap: map[string]string{ + "libcuda.so": "/lib/from/ldcache/libcuda.so.4.5.6", + }, + expected: []string{"/lib/from/ldcache/libcuda.so.4.5.6"}, + }, + { + description: "lib only not in LDCache returns error", + libname: "libnotcuda.so", + expectedError: errNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + // We override the LDCache with a mock implementation + l := lut.(*ldcacheLocator) + l.cache = &ldcache.LDCacheMock{ + LookupFunc: func(strings ...string) ([]string, []string) { + var result []string + for _, s := range strings { + if v, ok := tc.ldcacheMap[s]; ok { + result = append(result, v) + } + } + return nil, result + }, + } + + candidates, err := lut.Locate(tc.libname) + require.ErrorIs(t, err, tc.expectedError) + + var cleanedCandidates []string + for _, c := range candidates { + // On MacOS /var and /tmp symlink to /private/var and /private/tmp which is included in the resolved path. + cleanedCandidates = append(cleanedCandidates, strings.TrimPrefix(c, "/private")) + } + require.EqualValues(t, tc.expected, cleanedCandidates) + }) + } + +} + +func TestLibraryLocator(t *testing.T) { + logger, _ := testlog.NewNullLogger() + + testDir := t.TempDir() + symlinkDir := filepath.Join(testDir, "/lib/symlink") + require.NoError(t, os.MkdirAll(symlinkDir, 0755)) + + versionLib := filepath.Join(symlinkDir, "libcuda.so.1.2.3") + soLink := filepath.Join(symlinkDir, "libcuda.so") + sonameLink := filepath.Join(symlinkDir, "libcuda.so.1") + + f, err := os.Create(versionLib) + require.NoError(t, err) + f.Close() + require.NoError(t, os.Symlink(versionLib, sonameLink)) + require.NoError(t, os.Symlink(sonameLink, soLink)) + + // We create a set of symlinks for duplicate resolution + libTarget1 := filepath.Join(symlinkDir, "libtarget.so.1.2.3") + source1 := filepath.Join(symlinkDir, "libsource1.so") + source2 := filepath.Join(symlinkDir, "libsource2.so") + + target1, err := os.Create(libTarget1) + require.NoError(t, err) + target1.Close() + require.NoError(t, os.Symlink(libTarget1, source1)) + require.NoError(t, os.Symlink(source1, source2)) + + lut, err := NewLibraryLocator(logger, testDir) + require.NoError(t, err) + + testCases := []struct { + description string + libname string + expected []string + expectedError error + }{ + { + description: "slash in path resoves symlink", + libname: "/lib/symlink/libcuda.so", + expected: []string{filepath.Join(testDir, "/lib/symlink/libcuda.so.1.2.3")}, + }, + { + description: "slash in path resoves symlink", + libname: "/lib/symlink/libcuda.so.1", + expected: []string{filepath.Join(testDir, "/lib/symlink/libcuda.so.1.2.3")}, + }, + { + description: "slash in path with pattern resolves symlinks", + libname: "/lib/symlink/libcuda.so.*", + expected: []string{filepath.Join(testDir, "/lib/symlink/libcuda.so.1.2.3")}, + }, + { + description: "library not found returns error", + libname: "/lib/symlink/libnotcuda.so", + expectedError: errNotFound, + }, + { + description: "slash in path with pattern resoves symlink", + libname: "/lib/symlink/libcuda.so.*.*.*", + expected: []string{filepath.Join(testDir, "/lib/symlink/libcuda.so.1.2.3")}, + }, + { + description: "symlinks are deduplicated", + libname: "/lib/symlink/libsource*.so", + expected: []string{filepath.Join(testDir, "/lib/symlink/libtarget.so.1.2.3")}, + }, + { + description: "pattern resolves to multiple targets", + libname: "/lib/symlink/lib*.so.1.2.3", + expected: []string{ + filepath.Join(testDir, "/lib/symlink/libcuda.so.1.2.3"), + filepath.Join(testDir, "/lib/symlink/libtarget.so.1.2.3"), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + candidates, err := lut.Locate(tc.libname) + require.ErrorIs(t, err, tc.expectedError) + + var cleanedCandidates []string + for _, c := range candidates { + // On MacOS /var and /tmp symlink to /private/var and /private/tmp which is included in the resolved path. + cleanedCandidates = append(cleanedCandidates, strings.TrimPrefix(c, "/private")) + } + require.EqualValues(t, tc.expected, cleanedCandidates) + }) + } +} diff --git a/internal/lookup/locator.go b/internal/lookup/locator.go index 871e1b02..af5633fb 100644 --- a/internal/lookup/locator.go +++ b/internal/lookup/locator.go @@ -16,9 +16,13 @@ package lookup +import "errors" + //go:generate moq -stub -out locator_mock.go . Locator // Locator defines the interface for locating files on a system. type Locator interface { Locate(string) ([]string, error) } + +var errNotFound = errors.New("not found") diff --git a/internal/lookup/symlinks.go b/internal/lookup/symlinks.go index 495ad87e..41a0cf0e 100644 --- a/internal/lookup/symlinks.go +++ b/internal/lookup/symlinks.go @@ -119,7 +119,7 @@ func (p symlink) Locate(pattern string) ([]string, error) { } if len(targets) != 1 { - return nil, fmt.Errorf("failed to locate patern %q: failed to uniquely resolve symlink: %v", pattern, targets) + return nil, fmt.Errorf("failed to locate patern %q: %w; failed to uniquely resolve symlink: %v", pattern, errNotFound, candidates) } return targets, err }