Repository: coreruleset/go-ftw
Pull request #101 (2022-11-02 17:17:04)

chore: push changes to main
- ci: setup buildx for release docker builds
- fix: remove most uses of log.Fatal when running (#99)
- fix: remove obsolete use of tzdata (#95)
- fix: properly URL encode data for POST requests (#96)
- ci: add branch develop (#97)
- feature: look for ~/.ftw.yaml after looking in local folder


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 4506e74..46229dd 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
 
 on:
   push:
-    branches: [ main ]
+    branches: [ main, develop ]
   pull_request:
     # The branches below must be a subset of the branches above
-    branches: [ main ]
+    branches: [ main, develop ]
   schedule:
     - cron: '33 8 * * 4'
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0746e03..6f4ea0d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,6 +14,12 @@ jobs:
         uses: actions/checkout@v3
         with:
           fetch-depth: 0
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
       -
         name: Set up Go
         uses: actions/setup-go@v3
diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml
index 01f7625..97e1d4c 100644
--- a/.github/workflows/sonar.yaml
+++ b/.github/workflows/sonar.yaml
@@ -1,7 +1,7 @@
 name: Sonar
 on:
   push:
-    branches: [ main ]
+    branches: [ main, develop ]
 
 jobs:
   sonar:
diff --git a/check/base.go b/check/base.go
index 765f812..fea8dc2 100644
--- a/check/base.go
+++ b/check/base.go
@@ -60,7 +60,7 @@ func (c *FTWCheck) SetNoLogContains(contains string) {
 
 // ForcedIgnore check if this id need to be ignored from results
 func (c *FTWCheck) ForcedIgnore(id string) bool {
-	for re, _ := range c.overrides.Ignore {
+	for re := range c.overrides.Ignore {
 		if re.MatchString(id) {
 			return true
 		}
@@ -70,7 +70,7 @@ func (c *FTWCheck) ForcedIgnore(id string) bool {
 
 // ForcedPass check if this id need to be ignored from results
 func (c *FTWCheck) ForcedPass(id string) bool {
-	for re, _ := range c.overrides.ForcePass {
+	for re := range c.overrides.ForcePass {
 		if re.MatchString(id) {
 			return true
 		}
@@ -80,7 +80,7 @@ func (c *FTWCheck) ForcedPass(id string) bool {
 
 // ForcedFail check if this id need to be ignored from results
 func (c *FTWCheck) ForcedFail(id string) bool {
-	for re, _ := range c.overrides.ForceFail {
+	for re := range c.overrides.ForceFail {
 		if re.MatchString(id) {
 			return true
 		}
diff --git a/cmd/run.go b/cmd/run.go
index 6a9aeaa..3019ad6 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -56,7 +56,7 @@ var runCmd = &cobra.Command{
 			excludeRE = regexp.MustCompile(exclude)
 		}
 
-		currentRun := runner.Run(tests, runner.Config{
+		currentRun, err := runner.Run(tests, runner.Config{
 			Include:        includeRE,
 			Exclude:        excludeRE,
 			ShowTime:       showTime,
@@ -64,6 +64,9 @@ var runCmd = &cobra.Command{
 			ConnectTimeout: connectTimeout,
 			ReadTimeout:    readTimeout,
 		})
+		if err != nil {
+			log.Fatal().Err(err)
+		}
 
 		os.Exit(currentRun.Stats.TotalFailed())
 	},
diff --git a/config/config.go b/config/config.go
index ad91fa1..23de467 100644
--- a/config/config.go
+++ b/config/config.go
@@ -26,6 +26,20 @@ func NewConfigFromFile(cfgFile string) error {
 		cfgFile = ".ftw.yaml"
 	}
 
+	_, err = os.Stat(cfgFile)
+	if err != nil { // file does not exist, so we try the home folder
+
+        var home string
+
+		home, err = os.UserHomeDir()
+	    if err != nil { // home folder could not be retrieved
+		    return err
+	    }
+
+        cfgFile = home + "/.ftw.yaml"
+
+	}
+
 	_, err = os.Stat(cfgFile)
 	if err != nil { // file exists, so we read it looking for config values
 		return err
diff --git a/ftwhttp/client.go b/ftwhttp/client.go
index 729b78b..4d32e2b 100644
--- a/ftwhttp/client.go
+++ b/ftwhttp/client.go
@@ -21,17 +21,17 @@ func NewClientConfig() ClientConfig {
 }
 
 // NewClient initializes the http client, creating the cookiejar
-func NewClient(config ClientConfig) *Client {
+func NewClient(config ClientConfig) (*Client, error) {
 	// All users of cookiejar should import "golang.org/x/net/publicsuffix"
 	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
 	if err != nil {
-		log.Fatal().Err(err)
+		return nil, err
 	}
 	c := &Client{
 		Jar:    jar,
 		config: config,
 	}
-	return c
+	return c, nil
 }
 
 // NewConnection creates a new Connection based on a Destination
diff --git a/ftwhttp/connection.go b/ftwhttp/connection.go
index f02f333..d273780 100644
--- a/ftwhttp/connection.go
+++ b/ftwhttp/connection.go
@@ -5,6 +5,7 @@ import (
 	"bufio"
 	"bytes"
 	"errors"
+	"fmt"
 	"io"
 	"net"
 	"net/http"
@@ -82,7 +83,7 @@ func (c *Connection) Request(request *Request) error {
 	// Build request first, then connect and send, so timers are accurate
 	data, err := buildRequest(request)
 	if err != nil {
-		log.Fatal().Msgf("ftw/http: fatal error building request: %s", err.Error())
+		return fmt.Errorf("ftw/http: fatal error building request: %w", err)
 	}
 
 	log.Debug().Msgf("ftw/http: sending data:\n%s\n", data)
diff --git a/ftwhttp/request.go b/ftwhttp/request.go
index e84c836..23a80d7 100644
--- a/ftwhttp/request.go
+++ b/ftwhttp/request.go
@@ -183,33 +183,17 @@ func buildRequest(r *Request) ([]byte, error) {
 	return b.Bytes(), err
 }
 
-// If the values are empty in the map, then don't encode anythin
-// This keeps the compatibility with the python implementation
-func emptyQueryValues(values url.Values) bool {
-	for _, v := range values {
-		val := v
-		if len(val) > 1 {
-			return false
-		}
-	}
-	return true
-}
-
 // encodeDataParameters url encode parameters in data
 func encodeDataParameters(h Header, data []byte) ([]byte, error) {
 	var err error
 
 	if h.Get(ContentTypeHeader) == "application/x-www-form-urlencoded" {
-		if escapedData, _ := url.QueryUnescape(string(data)); escapedData == string(data) {
-			queryString, err := url.ParseQuery(string(data))
-			if (err != nil && strings.Contains(err.Error(), "invalid semicolon separator in query")) || emptyQueryValues(queryString) {
-				return data, nil
-			}
-			encodedData := queryString.Encode()
-			if encodedData != string(data) {
-				// we need to encode data
-				return []byte(encodedData), nil
+		if escapedData, err := url.QueryUnescape(string(data)); escapedData == string(data) {
+			if err != nil {
+				return nil, errors.New("Failed")
 			}
+			queryString := url.QueryEscape(string(data))
+			return []byte(queryString), nil
 		}
 	}
 	return data, err
diff --git a/main.go b/main.go
index 51f9f00..9313e84 100644
--- a/main.go
+++ b/main.go
@@ -5,7 +5,6 @@ package main
 import (
 	"fmt"
 	"os"
-	"time"
 	_ "time/tzdata"
 
 	"github.com/rs/zerolog"
@@ -29,15 +28,6 @@ func main() {
 	// Default level for this example is info, unless debug flag is present
 	zerolog.SetGlobalLevel(zerolog.InfoLevel)
 
-	// Load timezone location based on TZ
-	tzone, _ := time.Now().Zone()
-	loc, err := time.LoadLocation(tzone)
-	if err != nil {
-		log.Info().Msgf("ftw/main: cannot load timezone")
-	} else {
-		time.Local = loc // -> set the global timezone
-	}
-
 	cmd.Execute(
 		buildVersion(version, commit, date, builtBy),
 	)
diff --git a/runner/run.go b/runner/run.go
index 1671018..4c454ed 100644
--- a/runner/run.go
+++ b/runner/run.go
@@ -1,6 +1,7 @@
 package runner
 
 import (
+	"errors"
 	"fmt"
 	"regexp"
 	"time"
@@ -17,11 +18,16 @@ import (
 	"github.com/coreruleset/go-ftw/waflog"
 )
 
+var errBadTestRequest = errors.New("ftw/run: bad test: choose between data, encoded_request, or raw_request")
+
 // Run runs your tests with the specified Config. Returns error if some test failed
-func Run(tests []test.FTWTest, c Config) TestRunContext {
+func Run(tests []test.FTWTest, c Config) (TestRunContext, error) {
 	printUnlessQuietMode(c.Quiet, ":rocket:Running go-ftw!\n")
 
-	logLines := waflog.NewFTWLogLines(waflog.WithLogFile(config.FTWConfig.LogFile))
+	logLines, err := waflog.NewFTWLogLines(waflog.WithLogFile(config.FTWConfig.LogFile))
+	if err != nil {
+		return TestRunContext{}, err
+	}
 
 	conf := ftwhttp.NewClientConfig()
 	if c.ConnectTimeout != 0 {
@@ -30,7 +36,10 @@ func Run(tests []test.FTWTest, c Config) TestRunContext {
 	if c.ReadTimeout != 0 {
 		conf.ReadTimeout = c.ReadTimeout
 	}
-	client := ftwhttp.NewClient(conf)
+	client, err := ftwhttp.NewClient(conf)
+	if err != nil {
+		return TestRunContext{}, err
+	}
 	runContext := TestRunContext{
 		Include:  c.Include,
 		Exclude:  c.Exclude,
@@ -41,21 +50,23 @@ func Run(tests []test.FTWTest, c Config) TestRunContext {
 		RunMode:  config.FTWConfig.RunMode,
 	}
 
-	for _, test := range tests {
-		RunTest(&runContext, test)
+	for _, tc := range tests {
+		if err := RunTest(&runContext, tc); err != nil {
+			return TestRunContext{}, err
+		}
 	}
 
 	printSummary(c.Quiet, runContext.Stats)
 
 	defer cleanLogs(logLines)
 
-	return runContext
+	return runContext, nil
 }
 
 // RunTest runs an individual test.
 // runContext contains information for the current test run
 // ftwTest is the test you want to run
-func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) {
+func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) error {
 	changed := true
 
 	for _, testCase := range ftwTest.Tests {
@@ -78,9 +89,13 @@ func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) {
 		// Iterate over stages
 		for _, stage := range testCase.Stages {
 			ftwCheck := check.NewCheck(config.FTWConfig)
-			RunStage(runContext, ftwCheck, testCase, stage.Stage)
+			if err := RunStage(runContext, ftwCheck, testCase, stage.Stage); err != nil {
+				return err
+			}
 		}
 	}
+
+	return nil
 }
 
 // RunStage runs an individual test stage.
@@ -88,7 +103,7 @@ func RunTest(runContext *TestRunContext, ftwTest test.FTWTest) {
 // ftwCheck is the current check utility
 // testCase is the test case the stage belongs to
 // stage is the stage you want to run
-func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase test.Test, stage test.Stage) {
+func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase test.Test, stage test.Stage) error {
 	stageStartTime := time.Now()
 	stageID := uuid.NewString()
 	// Apply global overrides initially
@@ -101,14 +116,14 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes
 
 	// Check sanity first
 	if checkTestSanity(testRequest) {
-		log.Fatal().Msgf("ftw/run: bad test: choose between data, encoded_request, or raw_request")
+		return errBadTestRequest
 	}
 
 	// Do not even run test if result is overridden. Just use the override and display the overridden result.
 	if overridden := overriddenTestResult(ftwCheck, testCase.TestTitle); overridden != Failed {
 		addResultToStats(overridden, testCase.TestTitle, &runContext.Stats)
 		displayResult(runContext.Output, overridden, time.Duration(0), time.Duration(0))
-		return
+		return nil
 	}
 
 	var req *ftwhttp.Request
@@ -123,7 +138,7 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes
 	if notRunningInCloudMode(ftwCheck) {
 		startMarker, err := markAndFlush(runContext, dest, stageID)
 		if err != nil && !expectedOutput.ExpectError {
-			log.Fatal().Caller().Err(err).Msg("Failed to find start marker")
+			return fmt.Errorf("failed to find start marker: %w", err)
 		}
 		ftwCheck.SetStartMarker(startMarker)
 	}
@@ -133,7 +148,7 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes
 	err = runContext.Client.NewConnection(*dest)
 
 	if err != nil && !expectedOutput.ExpectError {
-		log.Fatal().Caller().Err(err).Msgf("can't connect to destination %+v", dest)
+		return fmt.Errorf("can't connect to destination %+v: %w", dest, err)
 	}
 	runContext.Client.StartTrackingTime()
 
@@ -141,13 +156,13 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes
 
 	runContext.Client.StopTrackingTime()
 	if responseErr != nil && !expectedOutput.ExpectError {
-		log.Fatal().Caller().Err(responseErr).Msgf("failed sending request to destination %+v", dest)
+		return fmt.Errorf("failed sending request to destination %+v: %w", dest, responseErr)
 	}
 
 	if notRunningInCloudMode(ftwCheck) {
 		endMarker, err := markAndFlush(runContext, dest, stageID)
 		if err != nil && !expectedOutput.ExpectError {
-			log.Fatal().Caller().Err(err).Msg("Failed to find end marker")
+			return fmt.Errorf("failed to find end marker: %w", err)
 
 		}
 		ftwCheck.SetEndMarker(endMarker)
@@ -171,6 +186,8 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes
 
 	runContext.Stats.Run++
 	runContext.Stats.RunTime += stageTime
+
+	return nil
 }
 
 func markAndFlush(runContext *TestRunContext, dest *ftwhttp.Destination, stageID string) ([]byte, error) {
@@ -386,6 +403,6 @@ func notRunningInCloudMode(c *check.FTWCheck) bool {
 
 func cleanLogs(logLines *waflog.FTWLogLines) {
 	if err := logLines.Cleanup(); err != nil {
-		log.Fatal().Err(err).Msg("Failed to cleanup log file")
+		log.Error().Err(err).Msg("Failed to cleanup log file")
 	}
 }
diff --git a/waflog/read.go b/waflog/read.go
index 91186cc..cb5f0eb 100644
--- a/waflog/read.go
+++ b/waflog/read.go
@@ -83,9 +83,6 @@ func (ll *FTWLogLines) getMarkedLines() [][]byte {
 // logFile is the file to search
 // stageID is the ID of the current stage, which is part of the marker line
 func (ll *FTWLogLines) CheckLogForMarker(stageID string) []byte {
-	if config.FTWConfig.RunMode == config.DefaultRunMode && ll.logFile == nil {
-		log.Fatal().Caller().Msg("No log file supplied")
-	}
 	offset, err := ll.logFile.Seek(0, os.SEEK_END)
 	if err != nil {
 		log.Error().Caller().Err(err).Msgf("failed to seek end of log file")
diff --git a/waflog/waflog.go b/waflog/waflog.go
index 7be09e1..da9b5f5 100644
--- a/waflog/waflog.go
+++ b/waflog/waflog.go
@@ -1,15 +1,17 @@
 package waflog
 
 import (
+	"errors"
+	"fmt"
 	"os"
 
-	"github.com/rs/zerolog/log"
-
 	"github.com/coreruleset/go-ftw/config"
 )
 
+var errNoLogFile = errors.New("no log file supplied")
+
 // NewFTWLogLines is the base struct for reading the log file
-func NewFTWLogLines(opts ...FTWLogOption) *FTWLogLines {
+func NewFTWLogLines(opts ...FTWLogOption) (*FTWLogLines, error) {
 	ll := &FTWLogLines{
 		logFile:     nil,
 		FileName:    config.FTWConfig.LogFile,
@@ -25,10 +27,14 @@ func NewFTWLogLines(opts ...FTWLogOption) *FTWLogLines {
 	}
 
 	if err := ll.openLogFile(); err != nil {
-		log.Error().Caller().Msgf("cannot open log file: %s", err)
+		return nil, fmt.Errorf("cannot open log file: %w", err)
+	}
+
+	if config.FTWConfig.RunMode == config.DefaultRunMode && ll.logFile == nil {
+		return nil, errNoLogFile
 	}
 
-	return ll
+	return ll, nil
 }
 
 // WithStartMarker sets the start marker for the log file


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f3bce0b..7ffbdf9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,10 +1,10 @@
 name: Test
 on:
   pull_request:
-    branches: [ main ]
+    branches: [ main, develop ]
     types: [opened, synchronize, reopened]
   push:
-    branches: [ main ]
+    branches: [ main, develop ]
 
 jobs:
   test:
diff --git a/ftwhttp/client_test.go b/ftwhttp/client_test.go
index 59cf6bb..d58102e 100644
--- a/ftwhttp/client_test.go
+++ b/ftwhttp/client_test.go
@@ -7,7 +7,8 @@ import (
 )
 
 func TestNewClient(t *testing.T) {
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 
 	assert.NotNil(t, c.Jar, "Error creating Client")
 }
@@ -19,9 +20,10 @@ func TestConnectDestinationHTTPS(t *testing.T) {
 		Protocol: "https",
 	}
 
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 
-	err := c.NewConnection(*d)
+	err = c.NewConnection(*d)
 	assert.NoError(t, err, "This should not error")
 	assert.Equal(t, "https", c.Transport.protocol, "Error connecting to example.com using https")
 }
@@ -33,11 +35,12 @@ func TestDoRequest(t *testing.T) {
 		Protocol: "https",
 	}
 
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 
 	req := generateBaseRequestForTesting()
 
-	err := c.NewConnection(*d)
+	err = c.NewConnection(*d)
 	assert.NoError(t, err, "This should not error")
 
 	_, err = c.Do(*req)
@@ -52,7 +55,8 @@ func TestGetTrackedTime(t *testing.T) {
 		Protocol: "https",
 	}
 
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 
 	rl := &RequestLine{
 		Method:  "POST",
@@ -65,7 +69,7 @@ func TestGetTrackedTime(t *testing.T) {
 	data := []byte(`test=me&one=two&one=twice`)
 	req := NewRequest(rl, h, data, true)
 
-	err := c.NewConnection(*d)
+	err = c.NewConnection(*d)
 	assert.NoError(t, err, "This should not error")
 
 	c.StartTrackingTime()
@@ -90,7 +94,8 @@ func TestClientMultipartFormDataRequest(t *testing.T) {
 		Protocol: "https",
 	}
 
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 
 	rl := &RequestLine{
 		Method:  "POST",
@@ -112,7 +117,7 @@ Some-file-test-here
 
 	req := NewRequest(rl, h, data, true)
 
-	err := c.NewConnection(*d)
+	err = c.NewConnection(*d)
 	assert.NoError(t, err, "This should not error")
 
 	c.StartTrackingTime()
@@ -127,7 +132,8 @@ Some-file-test-here
 }
 
 func TestNewConnectionCreatesTransport(t *testing.T) {
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 	assert.Nil(t, c.Transport, "Transport not expected to initialized yet")
 
 	server := testServer()
@@ -141,7 +147,8 @@ func TestNewConnectionCreatesTransport(t *testing.T) {
 }
 
 func TestNewOrReusedConnectionCreatesTransport(t *testing.T) {
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 	assert.Nil(t, c.Transport, "Transport not expected to initialized yet")
 
 	server := testServer()
@@ -155,7 +162,8 @@ func TestNewOrReusedConnectionCreatesTransport(t *testing.T) {
 }
 
 func TestNewOrReusedConnectionReusesTransport(t *testing.T) {
-	c := NewClient(NewClientConfig())
+	c, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 	assert.Nil(t, c.Transport, "Transport not expected to initialized yet")
 
 	server := testServer()
diff --git a/ftwhttp/request_test.go b/ftwhttp/request_test.go
index 2f02d69..2a15966 100644
--- a/ftwhttp/request_test.go
+++ b/ftwhttp/request_test.go
@@ -226,3 +226,25 @@ func TestRequestURLParseFail(t *testing.T) {
 	err := req.SetData([]byte("test=This&that=but with;;;;;; data now"))
 	assert.NoError(t, err)
 }
+
+func TestRequestEncodesPostData(t *testing.T) {
+	req := generateBaseRequestForTesting()
+
+	h := req.Headers()
+	h.Add(ContentTypeHeader, "application/x-www-form-urlencoded")
+	// Test adding semicolons to test parse
+	err := req.SetData([]byte(`c4= ;c3=t;c2=a;c1=c;a1=/;a2=e;a3=t;a4=c;a5=/;a6=p;a7=a;a8=s;a9=s;a10=w;a11=d;$c1$c2$c3$c4$a1$a2$a3$a4$a5$a6$a7$a8$a9$a10$a11`))
+	if err != nil {
+		t.Errorf("Failed !")
+	}
+	result, err := encodeDataParameters(h, req.Data())
+	if err != nil {
+		t.Errorf("Failed to encode %s", req.Data())
+	}
+
+	expected := "c4%3D+%3Bc3%3Dt%3Bc2%3Da%3Bc1%3Dc%3Ba1%3D%2F%3Ba2%3De%3Ba3%3Dt%3Ba4%3Dc%3Ba5%3D%2F%3Ba6%3Dp%3Ba7%3Da%3Ba8%3Ds%3Ba9%3Ds%3Ba10%3Dw%3Ba11%3Dd%3B%24c1%24c2%24c3%24c4%24a1%24a2%24a3%24a4%24a5%24a6%24a7%24a8%24a9%24a10%24a11"
+	actual := string(result)
+	if actual != expected {
+		t.Error("Unexpected URL encoded payload")
+	}
+}
diff --git a/ftwhttp/response_test.go b/ftwhttp/response_test.go
index 1503a46..868feb6 100644
--- a/ftwhttp/response_test.go
+++ b/ftwhttp/response_test.go
@@ -88,7 +88,8 @@ func TestResponse(t *testing.T) {
 
 	req := generateRequestForTesting(true)
 
-	client := NewClient(NewClientConfig())
+	client, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 	err = client.NewConnection(*d)
 	assert.NoError(t, err)
 
@@ -107,7 +108,8 @@ func TestResponseWithCookies(t *testing.T) {
 	assert.NoError(t, err)
 	req := generateRequestForTesting(true)
 
-	client := NewClient(NewClientConfig())
+	client, err := NewClient(NewClientConfig())
+	assert.NoError(t, err)
 	err = client.NewConnection(*d)
 
 	assert.NoError(t, err)
diff --git a/runner/run_test.go b/runner/run_test.go
index cf92d3d..f3ed92f 100644
--- a/runner/run_test.go
+++ b/runner/run_test.go
@@ -417,49 +417,55 @@ func TestRun(t *testing.T) {
 	replaceDestinationInTest(&ftwTest, *dest)
 
 	t.Run("show time and execute all", func(t *testing.T) {
-		res := Run([]test.FTWTest{ftwTest}, Config{
+		res, err := Run([]test.FTWTest{ftwTest}, Config{
 			ShowTime: true,
 			Quiet:    true,
 		})
+		assert.NoError(t, err)
 		assert.Equalf(t, res.Stats.TotalFailed(), 0, "Oops, %d tests failed to run!", res.Stats.TotalFailed())
 	})
 
 	t.Run("be verbose and execute all", func(t *testing.T) {
-		res := Run([]test.FTWTest{ftwTest}, Config{
+		res, err := Run([]test.FTWTest{ftwTest}, Config{
 			Include:  regexp.MustCompile("0*"),
 			ShowTime: true,
 		})
+		assert.NoError(t, err)
 		assert.Equal(t, res.Stats.TotalFailed(), 0, "verbose and execute all failed")
 	})
 
 	t.Run("don't show time and execute all", func(t *testing.T) {
-		res := Run([]test.FTWTest{ftwTest}, Config{
+		res, err := Run([]test.FTWTest{ftwTest}, Config{
 			Include: regexp.MustCompile("0*"),
 		})
+		assert.NoError(t, err)
 		assert.Equal(t, res.Stats.TotalFailed(), 0, "do not show time and execute all failed")
 	})
 
 	t.Run("execute only test 008 but exclude all", func(t *testing.T) {
-		res := Run([]test.FTWTest{ftwTest}, Config{
+		res, err := Run([]test.FTWTest{ftwTest}, Config{
 			Include: regexp.MustCompile("008"),
 			Exclude: regexp.MustCompile("0*"),
 		})
+		assert.NoError(t, err)
 		assert.Equal(t, res.Stats.TotalFailed(), 0, "do not show time and execute all failed")
 	})
 
 	t.Run("exclude test 010", func(t *testing.T) {
-		res := Run([]test.FTWTest{ftwTest}, Config{
+		res, err := Run([]test.FTWTest{ftwTest}, Config{
 			Exclude: regexp.MustCompile("010"),
 		})
+		assert.NoError(t, err)
 		assert.Equal(t, res.Stats.TotalFailed(), 0, "failed to exclude test")
 	})
 
 	t.Run("test exceptions 1", func(t *testing.T) {
-		res := Run([]test.FTWTest{ftwTest}, Config{
+		res, err := Run([]test.FTWTest{ftwTest}, Config{
 			Include: regexp.MustCompile("1*"),
 			Exclude: regexp.MustCompile("0*"),
 			Quiet:   true,
 		})
+		assert.NoError(t, err)
 		assert.Equal(t, res.Stats.TotalFailed(), 0, "failed to test exceptions")
 	})
 }
@@ -487,9 +493,10 @@ func TestOverrideRun(t *testing.T) {
 
 	replaceDestinationInTest(&ftwTest, *fakeDestination)
 
-	res := Run([]test.FTWTest{ftwTest}, Config{
+	res, err := Run([]test.FTWTest{ftwTest}, Config{
 		Quiet: true,
 	})
+	assert.NoError(t, err)
 	assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!")
 }
 
@@ -516,9 +523,10 @@ func TestBrokenOverrideRun(t *testing.T) {
 	replaceDestinationInTest(&ftwTest, *fakeDestination)
 
 	// the test should succeed, despite the unknown override property
-	res := Run([]test.FTWTest{ftwTest}, Config{
+	res, err := Run([]test.FTWTest{ftwTest}, Config{
 		Quiet: true,
 	})
+	assert.NoError(t, err)
 	assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!")
 }
 
@@ -547,16 +555,17 @@ func TestBrokenPortOverrideRun(t *testing.T) {
 	replaceDestinationInTest(&ftwTest, *fakeDestination)
 
 	// the test should succeed, despite the unknown override property
-	res := Run([]test.FTWTest{ftwTest}, Config{
+	res, err := Run([]test.FTWTest{ftwTest}, Config{
 		Quiet: true,
 	})
+	assert.NoError(t, err)
 	assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!")
 }
 
 func TestDisabledRun(t *testing.T) {
 	t.Cleanup(config.Reset)
 
-	err := config.NewConfigFromString(yamlConfig)
+	err := config.NewConfigFromString(yamlCloudConfig)
 	assert.NoError(t, err)
 
 	fakeDestination, err := ftwhttp.DestinationFromString("http://example.com:1234")
@@ -568,9 +577,10 @@ func TestDisabledRun(t *testing.T) {
 	assert.NoError(t, err)
 	replaceDestinationInTest(&ftwTest, *fakeDestination)
 
-	res := Run([]test.FTWTest{ftwTest}, Config{
+	res, err := Run([]test.FTWTest{ftwTest}, Config{
 		Quiet: true,
 	})
+	assert.NoError(t, err)
 	assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!")
 }
 
@@ -589,9 +599,10 @@ func TestLogsRun(t *testing.T) {
 	assert.NoError(t, err)
 	replaceDestinationInTest(&ftwTest, *dest)
 
-	res := Run([]test.FTWTest{ftwTest}, Config{
+	res, err := Run([]test.FTWTest{ftwTest}, Config{
 		Quiet: true,
 	})
+	assert.NoError(t, err)
 	assert.LessOrEqual(t, 0, res.Stats.TotalFailed(), "Oops, test run failed!")
 }
 
@@ -630,12 +641,14 @@ func TestCloudRun(t *testing.T) {
 
 				replaceDestinationInTest(&ftwTest, *dest)
 				assert.NoError(t, err)
+				client, err := ftwhttp.NewClient(ftwhttp.NewClientConfig())
+				assert.NoError(t, err)
 				runContext := TestRunContext{
 					Include:  nil,
 					Exclude:  nil,
 					ShowTime: false,
 					Output:   true,
-					Client:   ftwhttp.NewClient(ftwhttp.NewClientConfig()),
+					Client:   client,
 					LogLines: nil,
 				}
 
@@ -662,7 +675,8 @@ func TestFailedTestsRun(t *testing.T) {
 	assert.NoError(t, err)
 	replaceDestinationInTest(&ftwTest, *dest)
 
-	res := Run([]test.FTWTest{ftwTest}, Config{})
+	res, err := Run([]test.FTWTest{ftwTest}, Config{})
+	assert.NoError(t, err)
 	assert.Equal(t, 1, res.Stats.TotalFailed())
 }
 
@@ -709,6 +723,7 @@ func TestIgnoredTestsRun(t *testing.T) {
 
 	replaceDestinationInTest(&ftwTest, *dest)
 
-	res := Run([]test.FTWTest{ftwTest}, Config{})
+	res, err := Run([]test.FTWTest{ftwTest}, Config{})
+	assert.NoError(t, err)
 	assert.Equal(t, res.Stats.TotalFailed(), 1, "Oops, test run failed!")
 }
diff --git a/waflog/read_test.go b/waflog/read_test.go
index 3a6657c..d5d33c4 100644
--- a/waflog/read_test.go
+++ b/waflog/read_test.go
@@ -31,7 +31,8 @@ func TestReadCheckLogForMarkerNoMarkerAtEnd(t *testing.T) {
 	config.FTWConfig.LogFile = filename
 	t.Cleanup(func() { os.Remove(filename) })
 
-	ll := NewFTWLogLines(WithStartMarker(bytes.ToLower([]byte(markerLine))))
+	ll, err := NewFTWLogLines(WithStartMarker(bytes.ToLower([]byte(markerLine))))
+	assert.NoError(t, err)
 
 	marker := ll.CheckLogForMarker(stageID)
 	assert.Nil(t, marker, "unexpectedly found marker")
@@ -54,7 +55,8 @@ func TestReadCheckLogForMarkerWithMarkerAtEnd(t *testing.T) {
 	config.FTWConfig.LogFile = filename
 	t.Cleanup(func() { os.Remove(filename) })
 
-	ll := NewFTWLogLines(WithStartMarker(bytes.ToLower([]byte(markerLine))))
+	ll, err := NewFTWLogLines(WithStartMarker(bytes.ToLower([]byte(markerLine))))
+	assert.NoError(t, err)
 
 	marker := ll.CheckLogForMarker(stageID)
 	assert.NotNil(t, marker, "no marker found")
@@ -80,9 +82,10 @@ func TestReadGetMarkedLines(t *testing.T) {
 	config.FTWConfig.LogFile = filename
 	t.Cleanup(func() { os.Remove(filename) })
 
-	ll := NewFTWLogLines(
+	ll, err := NewFTWLogLines(
 		WithStartMarker(bytes.ToLower([]byte(startMarkerLine))),
 		WithEndMarker(bytes.ToLower([]byte(endMarkerLine))))
+	assert.NoError(t, err)
 
 	foundLines := ll.getMarkedLines()
 	// logs are scanned backwards
@@ -116,9 +119,10 @@ func TestReadGetMarkedLinesWithTrailingEmptyLines(t *testing.T) {
 	config.FTWConfig.LogFile = filename
 	t.Cleanup(func() { os.Remove(filename) })
 
-	ll := NewFTWLogLines(
+	ll, err := NewFTWLogLines(
 		WithStartMarker(bytes.ToLower([]byte(startMarkerLine))),
 		WithEndMarker(bytes.ToLower([]byte(endMarkerLine))))
+	assert.NoError(t, err)
 
 	foundLines := ll.getMarkedLines()
 	// logs are scanned backwards
@@ -155,9 +159,10 @@ func TestReadGetMarkedLinesWithPrecedingLines(t *testing.T) {
 	config.FTWConfig.LogFile = filename
 	t.Cleanup(func() { os.Remove(filename) })
 
-	ll := NewFTWLogLines(
+	ll, err := NewFTWLogLines(
 		WithStartMarker(bytes.ToLower([]byte(startMarkerLine))),
 		WithEndMarker(bytes.ToLower([]byte(endMarkerLine))))
+	assert.NoError(t, err)
 
 	foundLines := ll.getMarkedLines()
 	// logs are scanned backwards
diff --git a/waflog/waflog_test.go b/waflog/waflog_test.go
index 6e7979e..9677d80 100644
--- a/waflog/waflog_test.go
+++ b/waflog/waflog_test.go
@@ -12,7 +12,8 @@ func TestNewFTWLogLines(t *testing.T) {
 	err := config.NewConfigFromEnv()
 	assert.NoError(t, err)
 
-	ll := NewFTWLogLines()
+	// Don't call NewFTWLogLines to avoid opening the file.
+	ll := &FTWLogLines{}
 	// Loop through each option
 	for _, opt := range []FTWLogOption{
 		WithStartMarker([]byte("#")),