package agent
import (
"bufio"
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"testing"
"time"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/require"
)
func TestStringHash ( t * testing . T ) {
t . Parallel ( )
in := "hello world"
expected := "5eb63bbbe01eeed093cb22bb8f5acdc3"
if out := stringHash ( in ) ; out != expected {
t . Fatalf ( "bad: %s" , out )
}
}
func TestSetFilePermissions ( t * testing . T ) {
t . Parallel ( )
if runtime . GOOS == "windows" {
t . SkipNow ( )
}
tempFile := testutil . TempFile ( t , "consul" )
path := tempFile . Name ( )
// Bad UID fails
if err := setFilePermissions ( path , "%" , "" , "" ) ; err == nil {
t . Fatalf ( "should fail" )
}
// Bad GID fails
if err := setFilePermissions ( path , "" , "%" , "" ) ; err == nil {
t . Fatalf ( "should fail" )
}
// Bad mode fails
if err := setFilePermissions ( path , "" , "" , "%" ) ; err == nil {
t . Fatalf ( "should fail" )
}
// Allows omitting user/group/mode
if err := setFilePermissions ( path , "" , "" , "" ) ; err != nil {
t . Fatalf ( "err: %s" , err )
}
// Doesn't change mode if not given
if err := os . Chmod ( path , 0700 ) ; err != nil {
t . Fatalf ( "err: %s" , err )
}
if err := setFilePermissions ( path , "" , "" , "" ) ; err != nil {
t . Fatalf ( "err: %s" , err )
}
fi , err := os . Stat ( path )
if err != nil {
t . Fatalf ( "err: %s" , err )
}
if fi . Mode ( ) . String ( ) != "-rwx------" {
t . Fatalf ( "bad: %s" , fi . Mode ( ) )
}
// Changes mode if given
if err := setFilePermissions ( path , "" , "" , "0777" ) ; err != nil {
t . Fatalf ( "err: %s" , err )
}
fi , err = os . Stat ( path )
if err != nil {
t . Fatalf ( "err: %s" , err )
}
if fi . Mode ( ) . String ( ) != "-rwxrwxrwx" {
t . Fatalf ( "bad: %s" , fi . Mode ( ) )
}
}
func TestDurationFixer ( t * testing . T ) {
obj := map [ string ] interface { } {
"key1" : [ ] map [ string ] interface { } {
{
"subkey1" : "10s" ,
} ,
{
"subkey2" : "5d" ,
} ,
} ,
"key2" : map [ string ] interface { } {
"subkey3" : "30s" ,
"subkey4" : "20m" ,
} ,
"key3" : "11s" ,
"key4" : "49h" ,
}
expected := map [ string ] interface { } {
"key1" : [ ] map [ string ] interface { } {
{
"subkey1" : 10 * time . Second ,
} ,
{
"subkey2" : "5d" ,
} ,
} ,
"key2" : map [ string ] interface { } {
"subkey3" : "30s" ,
"subkey4" : 20 * time . Minute ,
} ,
"key3" : "11s" ,
"key4" : 49 * time . Hour ,
}
fixer := NewDurationFixer ( "key4" , "subkey1" , "subkey4" )
if err := fixer . FixupDurations ( obj ) ; err != nil {
t . Fatal ( err )
}
// Ensure we only processed the intended fieldnames
require . Equal ( t , expected , obj )
}
// helperProcessSentinel is a sentinel value that is put as the first
// argument following "--" and is used to determine if TestHelperProcess
// should run.
const helperProcessSentinel = "GO_WANT_HELPER_PROCESS"
// helperProcess returns an *exec.Cmd that can be used to execute the
// TestHelperProcess function below. This can be used to test multi-process
// interactions.
func helperProcess ( s ... string ) ( * exec . Cmd , func ( ) ) {
cs := [ ] string { "-test.run=TestHelperProcess" , "--" , helperProcessSentinel }
cs = append ( cs , s ... )
cmd := exec . Command ( os . Args [ 0 ] , cs ... )
destroy := func ( ) {
if p := cmd . Process ; p != nil {
p . Kill ( )
}
}
return cmd , destroy
}
// This is not a real test. This is just a helper process kicked off by tests
// using the helperProcess helper function.
func TestHelperProcess ( t * testing . T ) {
args := os . Args
for len ( args ) > 0 {
if args [ 0 ] == "--" {
args = args [ 1 : ]
break
}
args = args [ 1 : ]
}
if len ( args ) == 0 || args [ 0 ] != helperProcessSentinel {
return
}
defer os . Exit ( 0 )
args = args [ 1 : ] // strip sentinel value
cmd := args [ 0 ]
switch cmd {
case "parent-signal" :
// This subcommand forwards signals to a child process subcommand "print-signal".
limitProcessLifetime ( 2 * time . Minute )
cmd , destroy := helperProcess ( "print-signal" )
defer destroy ( )
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
if err := cmd . Start ( ) ; err != nil {
fmt . Fprintf ( os . Stderr , "child process failed to start: %v\n" , err )
os . Exit ( 1 )
}
doneCh := make ( chan struct { } )
defer func ( ) { close ( doneCh ) } ( )
logFn := func ( err error ) {
fmt . Fprintf ( os . Stderr , "could not forward signal: %s\n" , err )
os . Exit ( 1 )
}
ForwardSignals ( cmd , logFn , doneCh )
if err := cmd . Wait ( ) ; err != nil {
fmt . Fprintf ( os . Stderr , "unexpected error waiting for child: %v" , err )
os . Exit ( 1 )
}
case "print-signal" :
// This subcommand is instrumented to help verify signals are passed correctly.
limitProcessLifetime ( 2 * time . Minute )
ch := make ( chan os . Signal , 10 )
signal . Notify ( ch , forwardSignals ... )
defer signal . Stop ( ch )
fmt . Fprintf ( os . Stdout , "ready\n" )
s := <- ch
fmt . Fprintf ( os . Stdout , "signal: %s\n" , s )
default :
fmt . Fprintf ( os . Stderr , "Unknown command: %q\n" , cmd )
os . Exit ( 2 )
}
}
// limitProcessLifetime installs a background goroutine that self-exits after
// the specified duration elapses to prevent leaking processes from tests that
// may spawn them.
func limitProcessLifetime ( dur time . Duration ) {
go time . AfterFunc ( dur , func ( ) {
os . Exit ( 99 )
} )
}
func TestForwardSignals ( t * testing . T ) {
for _ , s := range forwardSignals {
t . Run ( "signal-" + s . String ( ) , func ( t * testing . T ) {
testForwardSignal ( t , s )
} )
}
}
func testForwardSignal ( t * testing . T , s os . Signal ) {
t . Helper ( )
if s == os . Kill {
t . Fatalf ( "you can't forward SIGKILL" )
}
// Launch a child process which registers the forwarding signal handler
// under test and then that in turn launches a grand child process that is
// our test instrument.
cmd , destroy := helperProcess ( "parent-signal" )
defer destroy ( )
cmd . Stderr = os . Stderr
prc , err := cmd . StdoutPipe ( )
if err != nil {
t . Fatalf ( "could not open stdout pipe for child process: %v" , err )
}
defer prc . Close ( )
if err := cmd . Start ( ) ; err != nil {
t . Fatalf ( "child process failed to start: %v" , err )
}
scan := bufio . NewScanner ( prc )
// Wait until the grandchild relays back to us that it's ready to receive
// signals.
expectLine ( t , "ready" , scan )
// Relay our chosen signal down through the intermediary process.
if err := cmd . Process . Signal ( s ) ; err != nil {
t . Fatalf ( "signalling child failed: %v" , err )
}
// Verify that the signal we intended made it all the way to the grandchild.
expectLine ( t , "signal: " + s . String ( ) , scan )
}
func expectLine ( t * testing . T , expect string , scan * bufio . Scanner ) {
if ! scan . Scan ( ) {
if scan . Err ( ) != nil {
t . Fatalf ( "expected to read line %q but failed: %v" , expect , scan . Err ( ) )
} else {
t . Fatalf ( "expected to read line %q but got no line" , expect )
}
}
if line := scan . Text ( ) ; expect != line {
t . Fatalf ( "expected to read line %q but got %q" , expect , line )
}
}