Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions internal/fileutils/fileutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"io"
"os"
"path/filepath"

"golang.org/x/sys/unix"
)

// FileExists checks if a file exists at the given path.
Expand Down Expand Up @@ -103,3 +105,28 @@ func Lrename(oldPath, newPath string) error {

return os.Rename(oldPath, newPath)
}

// LockDir creates a lock file in the specified directory and acquires an exclusive lock on it.
// It blocks until the lock is available and returns an unlock function to release the lock.
func LockDir(dir string) (func() error, error) {
lockPath := filepath.Join(dir, ".lock")
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}

if err := unix.Flock(int(f.Fd()), unix.LOCK_EX); err != nil {
_ = f.Close()
return nil, err
}

unlock := func() error {
if err := unix.Flock(int(f.Fd()), unix.LOCK_UN); err != nil {
_ = f.Close()
return err
}
return f.Close()
}

return unlock, nil
}
44 changes: 44 additions & 0 deletions internal/fileutils/fileutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/ubuntu/authd/internal/fileutils"
"github.com/ubuntu/authd/internal/testutils"
)

// errAny represents any error type, for testing purposes.
Expand Down Expand Up @@ -352,3 +354,45 @@ func TestLrename(t *testing.T) {
})
}
}

func TestLockDir(t *testing.T) {
t.Parallel()

tempDir := t.TempDir()

// First lock should succeed
t.Log("Acquiring first lock")
unlock, err := fileutils.LockDir(tempDir)
require.NoError(t, err, "LockDir should not return an error")

// Second lock should block, so we run it in a goroutine and check it doesn't return immediately
unlockCh := make(chan func() error, 1)
go func() {
t.Log("Acquiring second lock (should block)")
unlock2, err := fileutils.LockDir(tempDir)
t.Logf("Second LockDir returned with error: %v", err)
unlockCh <- unlock2
}()
select {
case <-unlockCh:
require.Fail(t, "LockDir should block when trying to lock an already locked directory")
case <-time.After(testutils.MultipliedSleepDuration(100 * time.Millisecond)):
// Expected behavior, LockDir is blocking
}

// Unlock the first lock
t.Log("Releasing first lock")
err = unlock()
require.NoError(t, err, "Unlock should not return an error")

// Now we should be able to acquire the lock again
select {
case unlock = <-unlockCh:
// Expected behavior, LockDir returned after the first lock was released
t.Log("Releasing lock")
err = unlock()
require.NoError(t, err, "Unlock should not return an error")
case <-time.After(testutils.MultipliedSleepDuration(5 * time.Second)):
require.Fail(t, "LockDir should have returned after the first lock was released")
}
}
16 changes: 16 additions & 0 deletions internal/testutils/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,24 @@ var (
isRaceOnce sync.Once
sleepMultiplier float64
sleepMultiplierOnce sync.Once
testVerbosity int
testVerbosityOnce sync.Once
)

// TestVerbosity returns the verbosity level that should be used in tests.
func TestVerbosity() int {
testVerbosityOnce.Do(func() {
if v := os.Getenv("AUTHD_TEST_VERBOSITY"); v != "" {
var err error
testVerbosity, err = strconv.Atoi(v)
if err != nil {
panic(err)
}
}
})
return testVerbosity
}

func haveBuildFlag(flag string) bool {
b, ok := debug.ReadBuildInfo()
if !ok {
Expand Down
35 changes: 24 additions & 11 deletions internal/testutils/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ubuntu/authd/internal/fileutils"
)

func getCargoPath() (path string, isNightly bool, err error) {
Expand Down Expand Up @@ -74,35 +75,46 @@ func BuildRustNSSLib(t *testing.T, disableCoverage bool, features ...string) (li
cargo, isNightly, err := getCargoPath()
require.NoError(t, err, "Setup: looking for cargo")

// Note that for developing purposes and avoiding keeping building the rust program dependencies,
// TEST_RUST_TARGET environment variable can be set to an absolute path to keep iterative
// build artifacts.
// Store the build artifacts in a common temp directory, so that they can be reused between tests.
target := os.Getenv("TEST_RUST_TARGET")
if target == "" {
target = t.TempDir()
target = filepath.Join(os.TempDir(), "authd-tests-rust-build-artifacts")
}

err = os.MkdirAll(target, 0700)
require.NoError(t, err, "Setup: could not create Rust target dir")

rustDir := filepath.Join(projectRoot, "nss")
if !disableCoverage {
rustCovEnv = trackRustCoverage(t, target, rustDir)
}

features = append([]string{"integration_tests", "custom_socket"}, features...)

unlock, err := fileutils.LockDir(target)
require.NoError(t, err, "Setup: could not lock Rust target dir")
defer func() {
require.NoError(t, unlock(), "Setup: could not unlock Rust target dir")
}()

// Builds the nss library.
// #nosec:G204 - we control the command arguments in tests
cmd := exec.Command(cargo, "build", "--verbose",
"--features", strings.Join(features, ","), "--target-dir", target)
cmd := exec.Command(cargo, "build", "--features", strings.Join(features, ","), "--target-dir", target)
if TestVerbosity() > 0 {
cmd.Args = append(cmd.Args, "--verbose")
}
cmd.Env = append(os.Environ(), rustCovEnv...)
cmd.Dir = projectRoot
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if isNightly && IsAsan() {
cmd.Env = append(cmd.Env, "RUSTFLAGS=-Zsanitizer=address")
}

t.Log("Building NSS library...", cmd.Args)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "Setup: could not build Rust NSS library: %s", out)
err = cmd.Run()
require.NoError(t, err, "Setup: could not build Rust NSS library")

// When building the crate with dh-cargo, this env is set to indicate which architecture the code
// is being compiled to. When it's set, the compiled is stored under target/$(DEB_HOST_RUST_TYPE)/debug,
Expand All @@ -111,9 +123,10 @@ func BuildRustNSSLib(t *testing.T, disableCoverage bool, features ...string) (li
// If the env is not set, the target stays the same.
target = filepath.Join(target, os.Getenv("DEB_HOST_RUST_TYPE"))

// Creates a symlink for the compiled library with the expected versioned name.
libPath = filepath.Join(target, "libnss_authd.so.2")
if err = os.Symlink(filepath.Join(target, "debug", "libnss_authd.so"), libPath); err != nil {
// Copy the library with the expected versioned name to a temporary directory, so that we can safely use
// it from there after unlocking the target directory, which allows other tests to rebuild the library.
libPath = filepath.Join(t.TempDir(), "libnss_authd.so.2")
if err = fileutils.CopyFile(filepath.Join(target, "debug", "libnss_authd.so"), libPath); err != nil {
require.ErrorIs(t, err, os.ErrExist, "Setup: failed to create versioned link to the library")
}

Expand Down
5 changes: 4 additions & 1 deletion nss/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(false)
.protoc_arg("--experimental_allow_proto3_optional")
.compile_protos(&["../internal/proto/authd/authd.proto"], &["../"])?;
.compile_protos(
&["../internal/proto/authd/authd.proto"],
&["../internal/proto"],
)?;

#[cfg(feature = "integration_tests")]
cc::Build::new()
Expand Down
Loading