// Copyright (c) Mainflux // SPDX-License-Identifier: Apache-2.0 package logger_test import ( "encoding/json" "fmt" "io" "os" "os/exec" "testing" mflog "github.com/mainflux/mainflux/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Env vars needed for testing Fatal in subprocess. const ( testMsg = "TEST_MSG" testFlag = "TEST_FLAG" testFlagVal = "assert_test" ) var _ io.Writer = (*mockWriter)(nil) var logger mflog.Logger var err error var output logMsg type mockWriter struct { value []byte } func (writer *mockWriter) Write(p []byte) (int, error) { writer.value = p return len(p), nil } func (writer *mockWriter) Read() (logMsg, error) { var output logMsg err := json.Unmarshal(writer.value, &output) return output, err } type logMsg struct { Level string `json:"level"` Message string `json:"message"` Fatal string `json:"fatal,omitempty"` // needed for Fatal messages } func TestDebug(t *testing.T) { cases := []struct { desc string input string level string output logMsg }{ { desc: "debug log ordinary string", input: "input_string", level: mflog.Debug.String(), output: logMsg{mflog.Debug.String(), "input_string", ""}, }, { desc: "debug log empty string", input: "", level: mflog.Debug.String(), output: logMsg{mflog.Debug.String(), "", ""}, }, { desc: "debug ordinary string lvl not allowed", input: "input_string", level: mflog.Info.String(), output: logMsg{"", "", ""}, }, { desc: "debug empty string lvl not allowed", input: "", level: mflog.Info.String(), output: logMsg{"", "", ""}, }, } for _, tc := range cases { writer := mockWriter{} logger, err = mflog.New(&writer, tc.level) assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) logger.Debug(tc.input) output, err = writer.Read() assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.output, output)) } } func TestInfo(t *testing.T) { cases := []struct { desc string input string level string output logMsg }{ { desc: "info log ordinary string", input: "input_string", level: mflog.Info.String(), output: logMsg{mflog.Info.String(), "input_string", ""}, }, { desc: "info log empty string", input: "", level: mflog.Info.String(), output: logMsg{mflog.Info.String(), "", ""}, }, { desc: "info ordinary string lvl not allowed", input: "input_string", level: mflog.Warn.String(), output: logMsg{"", "", ""}, }, { desc: "info empty string lvl not allowed", input: "", level: mflog.Warn.String(), output: logMsg{"", "", ""}, }, } for _, tc := range cases { writer := mockWriter{} logger, err = mflog.New(&writer, tc.level) assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) logger.Info(tc.input) output, err = writer.Read() assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.output, output)) } } func TestWarn(t *testing.T) { cases := []struct { desc string input string level string output logMsg }{ { desc: "warn log ordinary string", input: "input_string", level: mflog.Warn.String(), output: logMsg{mflog.Warn.String(), "input_string", ""}, }, { desc: "warn log empty string", input: "", level: mflog.Warn.String(), output: logMsg{mflog.Warn.String(), "", ""}, }, { desc: "warn ordinary string lvl not allowed", input: "input_string", level: mflog.Error.String(), output: logMsg{"", "", ""}, }, { desc: "warn empty string lvl not allowed", input: "", level: mflog.Error.String(), output: logMsg{"", "", ""}, }, } for _, tc := range cases { writer := mockWriter{} logger, err = mflog.New(&writer, tc.level) require.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) logger.Warn(tc.input) output, err = writer.Read() assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.output, output)) } } func TestError(t *testing.T) { cases := []struct { desc string input string output logMsg }{ { desc: "error log ordinary string", input: "input_string", output: logMsg{mflog.Error.String(), "input_string", ""}, }, { desc: "error log empty string", input: "", output: logMsg{mflog.Error.String(), "", ""}, }, } writer := mockWriter{} logger, err := mflog.New(&writer, mflog.Error.String()) require.Nil(t, err) for _, tc := range cases { logger.Error(tc.input) output, err := writer.Read() assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.output, output)) } } func TestFatal(t *testing.T) { // This is the actually Fatal call we test that will // be executed in the subprocess spawned by the test. if os.Getenv(testFlag) == testFlagVal { logger, err := mflog.New(os.Stderr, mflog.Error.String()) require.Nil(t, err) msg := os.Getenv(testMsg) logger.Fatal(msg) return } cases := []struct { desc string input string output logMsg }{ { desc: "error log ordinary string", input: "input_string", output: logMsg{"", "", "input_string"}, }, { desc: "error log empty string", input: "", output: logMsg{"", "", ""}, }, } writer := mockWriter{} for _, tc := range cases { // This command will run this same test as a separate subprocess. // It needs to be executed as a subprocess because we need to test os.Exit(1) call. cmd := exec.Command(os.Args[0], "-test.run=TestFatal") // This flag is used to prevent an infinite loop of spawning this test and never // actually running the necessary Fatal call. cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", testFlag, testFlagVal)) cmd.Stderr = &writer cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", testMsg, tc.input)) err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { res, err := writer.Read() require.Nil(t, err, "required successful buffer read") assert.Equal(t, 1, e.ExitCode(), fmt.Sprintf("%s: expected exit code %d, got %d", tc.desc, 1, e.ExitCode())) assert.Equal(t, tc.output, res, fmt.Sprintf("%s: expected output %s got %s", tc.desc, tc.output, res)) continue } t.Fatal("subprocess ran successfully, want non-zero exit status") } }