diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 159584d..54154ca 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,10 @@ "Bash(go test:*)", "Bash(git add:*)", "Bash(make lint:*)", - "Bash(golangci-lint run:*)" + "Bash(golangci-lint run:*)", + "Bash(rm:*)", + "Bash(git rm:*)", + "Bash(grep:*)" ], "deny": [] } diff --git a/Makefile b/Makefile index cecf187..61d9cf8 100644 --- a/Makefile +++ b/Makefile @@ -7,5 +7,5 @@ lint: .PHONY: test test: which go-test-coverage || go install github.com/vladopajic/go-test-coverage/v2@latest - go test -v ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... + go test -v ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... -json | python3 testutil/colourise-go-test-output.py go-test-coverage --config=./.testcoverage.yaml diff --git a/config_internal_test.go b/config_internal_test.go new file mode 100644 index 0000000..108e845 --- /dev/null +++ b/config_internal_test.go @@ -0,0 +1,148 @@ +package hal + +import ( + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" +) + +func TestSearchParentsForFile(t *testing.T) { + t.Parallel() + + t.Run("finds file in current directory", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + testFile := "test.txt" + testPath := filepath.Join(tmpDir, testFile) + + err := os.WriteFile(testPath, []byte("test"), 0o644) + assert.NilError(t, err) + + foundPath, err := searchParentsForFile(testFile, tmpDir) + assert.NilError(t, err) + assert.Equal(t, foundPath, testPath) + }) + + t.Run("finds file in parent directory", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + subDir := filepath.Join(tmpDir, "subdir") + err := os.Mkdir(subDir, 0o755) + assert.NilError(t, err) + + testFile := "test.txt" + testPath := filepath.Join(tmpDir, testFile) + + err = os.WriteFile(testPath, []byte("test"), 0o644) + assert.NilError(t, err) + + foundPath, err := searchParentsForFile(testFile, subDir) + assert.NilError(t, err) + assert.Equal(t, foundPath, testPath) + }) + + t.Run("returns empty string when file not found", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + foundPath, err := searchParentsForFile("nonexistent.txt", tmpDir) + assert.NilError(t, err) + assert.Equal(t, foundPath, "") + }) +} + +//nolint:paralleltest // This test changes working directory and cannot run in parallel +func TestSearchParentsForFileFromCwd(t *testing.T) { + t.Run("searches from current working directory", func(t *testing.T) { + tmpDir := t.TempDir() + originalDir, _ := os.Getwd() + defer os.Chdir(originalDir) + + testFile := "test.txt" + testPath := filepath.Join(tmpDir, testFile) + + err := os.WriteFile(testPath, []byte("test"), 0o644) + assert.NilError(t, err) + + err = os.Chdir(tmpDir) + assert.NilError(t, err) + + foundPath, err := searchParentsForFileFromCwd(testFile) + assert.NilError(t, err) + + // Resolve symlinks to handle macOS /private/var vs /var differences + resolvedFound, err := filepath.EvalSymlinks(foundPath) + assert.NilError(t, err) + resolvedExpected, err := filepath.EvalSymlinks(testPath) + assert.NilError(t, err) + + assert.Equal(t, resolvedFound, resolvedExpected) + }) +} + +func TestGetParents(t *testing.T) { + t.Parallel() + + t.Run("returns correct parent paths", func(t *testing.T) { + t.Parallel() + if filepath.Separator == '\\' { + // Windows paths + paths := getParents("C:\\Users\\test\\project") + expected := []string{ + "C:\\Users\\test\\project", + "C:\\Users\\test", + "C:\\Users", + "C:\\", + "/", + } + assert.DeepEqual(t, paths, expected) + } else { + // Unix paths + paths := getParents("/home/user/project") + expected := []string{ + "/home/user/project", + "/home/user", + "/home", + "/", + } + assert.DeepEqual(t, paths, expected) + } + }) + + t.Run("handles root directory", func(t *testing.T) { + t.Parallel() + paths := getParents("/") + expected := []string{"/"} + assert.DeepEqual(t, paths, expected) + }) +} + +func TestFileExists(t *testing.T) { + t.Parallel() + + t.Run("returns true for existing file", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + testPath := filepath.Join(tmpDir, "test.txt") + + err := os.WriteFile(testPath, []byte("test"), 0o644) + assert.NilError(t, err) + + exists := fileExists(testPath) + assert.Equal(t, exists, true) + }) + + t.Run("returns false for non-existent file", func(t *testing.T) { + t.Parallel() + exists := fileExists("/nonexistent/path/file.txt") + assert.Equal(t, exists, false) + }) + + t.Run("returns true for existing directory", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + exists := fileExists(tmpDir) + assert.Equal(t, exists, true) + }) +} diff --git a/config_test.go b/config_test.go index 4ab1200..bfeb9a4 100644 --- a/config_test.go +++ b/config_test.go @@ -1,13 +1,15 @@ -package hal +package hal_test import ( "os" "path/filepath" "testing" + "github.com/dansimau/hal" "gotest.tools/v3/assert" ) +//nolint:paralleltest // This test changes working directory and cannot run in parallel func TestLoadConfig(t *testing.T) { t.Run("loads valid config", func(t *testing.T) { // Create a temporary directory and config file @@ -24,14 +26,14 @@ location: lng: -122.4194` configPath := filepath.Join(tmpDir, "hal.yaml") - err := os.WriteFile(configPath, []byte(configContent), 0644) + err := os.WriteFile(configPath, []byte(configContent), 0o644) assert.NilError(t, err) // Change to temp directory err = os.Chdir(tmpDir) assert.NilError(t, err) - config, err := LoadConfig() + config, err := hal.LoadConfig() assert.NilError(t, err) assert.Equal(t, config.HomeAssistant.Host, "localhost:8123") assert.Equal(t, config.HomeAssistant.Token, "test-token") @@ -48,7 +50,7 @@ location: err := os.Chdir(tmpDir) assert.NilError(t, err) - _, err = LoadConfig() + _, err = hal.LoadConfig() assert.ErrorContains(t, err, "no such file or directory") }) @@ -59,137 +61,13 @@ location: invalidContent := `invalid: yaml: content:` configPath := filepath.Join(tmpDir, "hal.yaml") - err := os.WriteFile(configPath, []byte(invalidContent), 0644) + err := os.WriteFile(configPath, []byte(invalidContent), 0o644) assert.NilError(t, err) err = os.Chdir(tmpDir) assert.NilError(t, err) - _, err = LoadConfig() + _, err = hal.LoadConfig() assert.ErrorContains(t, err, "yaml") }) } - -func TestSearchParentsForFile(t *testing.T) { - t.Run("finds file in current directory", func(t *testing.T) { - tmpDir := t.TempDir() - testFile := "test.txt" - testPath := filepath.Join(tmpDir, testFile) - - err := os.WriteFile(testPath, []byte("test"), 0644) - assert.NilError(t, err) - - foundPath, err := searchParentsForFile(testFile, tmpDir) - assert.NilError(t, err) - assert.Equal(t, foundPath, testPath) - }) - - t.Run("finds file in parent directory", func(t *testing.T) { - tmpDir := t.TempDir() - subDir := filepath.Join(tmpDir, "subdir") - err := os.Mkdir(subDir, 0755) - assert.NilError(t, err) - - testFile := "test.txt" - testPath := filepath.Join(tmpDir, testFile) - - err = os.WriteFile(testPath, []byte("test"), 0644) - assert.NilError(t, err) - - foundPath, err := searchParentsForFile(testFile, subDir) - assert.NilError(t, err) - assert.Equal(t, foundPath, testPath) - }) - - t.Run("returns empty string when file not found", func(t *testing.T) { - tmpDir := t.TempDir() - foundPath, err := searchParentsForFile("nonexistent.txt", tmpDir) - assert.NilError(t, err) - assert.Equal(t, foundPath, "") - }) -} - -func TestSearchParentsForFileFromCwd(t *testing.T) { - t.Run("searches from current working directory", func(t *testing.T) { - tmpDir := t.TempDir() - originalDir, _ := os.Getwd() - defer os.Chdir(originalDir) - - testFile := "test.txt" - testPath := filepath.Join(tmpDir, testFile) - - err := os.WriteFile(testPath, []byte("test"), 0644) - assert.NilError(t, err) - - err = os.Chdir(tmpDir) - assert.NilError(t, err) - - foundPath, err := searchParentsForFileFromCwd(testFile) - assert.NilError(t, err) - - // Resolve symlinks to handle macOS /private/var vs /var differences - resolvedFound, err := filepath.EvalSymlinks(foundPath) - assert.NilError(t, err) - resolvedExpected, err := filepath.EvalSymlinks(testPath) - assert.NilError(t, err) - - assert.Equal(t, resolvedFound, resolvedExpected) - }) -} - -func TestGetParents(t *testing.T) { - t.Run("returns correct parent paths", func(t *testing.T) { - if filepath.Separator == '\\' { - // Windows paths - paths := getParents("C:\\Users\\test\\project") - expected := []string{ - "C:\\Users\\test\\project", - "C:\\Users\\test", - "C:\\Users", - "C:\\", - "/", - } - assert.DeepEqual(t, paths, expected) - } else { - // Unix paths - paths := getParents("/home/user/project") - expected := []string{ - "/home/user/project", - "/home/user", - "/home", - "/", - } - assert.DeepEqual(t, paths, expected) - } - }) - - t.Run("handles root directory", func(t *testing.T) { - paths := getParents("/") - expected := []string{"/"} - assert.DeepEqual(t, paths, expected) - }) -} - -func TestFileExists(t *testing.T) { - t.Run("returns true for existing file", func(t *testing.T) { - tmpDir := t.TempDir() - testPath := filepath.Join(tmpDir, "test.txt") - - err := os.WriteFile(testPath, []byte("test"), 0644) - assert.NilError(t, err) - - exists := fileExists(testPath) - assert.Equal(t, exists, true) - }) - - t.Run("returns false for non-existent file", func(t *testing.T) { - exists := fileExists("/nonexistent/path/file.txt") - assert.Equal(t, exists, false) - }) - - t.Run("returns true for existing directory", func(t *testing.T) { - tmpDir := t.TempDir() - exists := fileExists(tmpDir) - assert.Equal(t, exists, true) - }) -} \ No newline at end of file diff --git a/entity_binary_sensor_test.go b/entity_binary_sensor_test.go index 8ae4b65..10c2091 100644 --- a/entity_binary_sensor_test.go +++ b/entity_binary_sensor_test.go @@ -1,20 +1,26 @@ -package hal +package hal_test import ( "testing" + "github.com/dansimau/hal" "github.com/dansimau/hal/homeassistant" "gotest.tools/v3/assert" ) func TestNewBinarySensor(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + + sensor := hal.NewBinarySensor("binary_sensor.test") assert.Equal(t, sensor.GetID(), "binary_sensor.test") } func TestBinarySensor_IsOff(t *testing.T) { + t.Parallel() + t.Run("returns true when state is off", func(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + sensor := hal.NewBinarySensor("binary_sensor.test") state := homeassistant.State{State: "off"} sensor.SetState(state) @@ -22,7 +28,8 @@ func TestBinarySensor_IsOff(t *testing.T) { }) t.Run("returns false when state is on", func(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + sensor := hal.NewBinarySensor("binary_sensor.test") state := homeassistant.State{State: "on"} sensor.SetState(state) @@ -30,7 +37,8 @@ func TestBinarySensor_IsOff(t *testing.T) { }) t.Run("returns false when state is other value", func(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + sensor := hal.NewBinarySensor("binary_sensor.test") state := homeassistant.State{State: "unavailable"} sensor.SetState(state) @@ -39,8 +47,11 @@ func TestBinarySensor_IsOff(t *testing.T) { } func TestBinarySensor_IsOn(t *testing.T) { + t.Parallel() + t.Run("returns true when state is on", func(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + sensor := hal.NewBinarySensor("binary_sensor.test") state := homeassistant.State{State: "on"} sensor.SetState(state) @@ -48,7 +59,8 @@ func TestBinarySensor_IsOn(t *testing.T) { }) t.Run("returns false when state is off", func(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + sensor := hal.NewBinarySensor("binary_sensor.test") state := homeassistant.State{State: "off"} sensor.SetState(state) @@ -56,10 +68,11 @@ func TestBinarySensor_IsOn(t *testing.T) { }) t.Run("returns false when state is other value", func(t *testing.T) { - sensor := NewBinarySensor("binary_sensor.test") + t.Parallel() + sensor := hal.NewBinarySensor("binary_sensor.test") state := homeassistant.State{State: "unknown"} sensor.SetState(state) assert.Equal(t, sensor.IsOn(), false) }) -} \ No newline at end of file +} diff --git a/entity_input_boolean_test.go b/entity_input_boolean_test.go index d3f0b2d..f58d29b 100644 --- a/entity_input_boolean_test.go +++ b/entity_input_boolean_test.go @@ -1,20 +1,24 @@ -package hal +package hal_test import ( "testing" + "github.com/dansimau/hal" "github.com/dansimau/hal/homeassistant" "gotest.tools/v3/assert" ) func TestNewInputBoolean(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") assert.Equal(t, inputBoolean.GetID(), "input_boolean.test") } func TestInputBoolean_IsOff(t *testing.T) { + t.Parallel() t.Run("returns true when state is off", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") state := homeassistant.State{State: "off"} inputBoolean.SetState(state) @@ -22,7 +26,8 @@ func TestInputBoolean_IsOff(t *testing.T) { }) t.Run("returns false when state is on", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") state := homeassistant.State{State: "on"} inputBoolean.SetState(state) @@ -30,7 +35,8 @@ func TestInputBoolean_IsOff(t *testing.T) { }) t.Run("returns false when state is other value", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") state := homeassistant.State{State: "unavailable"} inputBoolean.SetState(state) @@ -39,8 +45,10 @@ func TestInputBoolean_IsOff(t *testing.T) { } func TestInputBoolean_IsOn(t *testing.T) { + t.Parallel() t.Run("returns true when state is on", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") state := homeassistant.State{State: "on"} inputBoolean.SetState(state) @@ -48,7 +56,8 @@ func TestInputBoolean_IsOn(t *testing.T) { }) t.Run("returns false when state is off", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") state := homeassistant.State{State: "off"} inputBoolean.SetState(state) @@ -56,7 +65,8 @@ func TestInputBoolean_IsOn(t *testing.T) { }) t.Run("returns false when state is other value", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") state := homeassistant.State{State: "unknown"} inputBoolean.SetState(state) @@ -65,23 +75,28 @@ func TestInputBoolean_IsOn(t *testing.T) { } func TestInputBoolean_TurnOn(t *testing.T) { + t.Parallel() t.Run("returns error when not registered", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") err := inputBoolean.TurnOn() - assert.Equal(t, err, ErrEntityNotRegistered) + assert.Equal(t, err, hal.ErrEntityNotRegistered) }) t.Run("accepts attributes", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") err := inputBoolean.TurnOn(map[string]any{"custom": "value"}) - assert.Equal(t, err, ErrEntityNotRegistered) + assert.Equal(t, err, hal.ErrEntityNotRegistered) }) } func TestInputBoolean_TurnOff(t *testing.T) { + t.Parallel() t.Run("returns error when not registered", func(t *testing.T) { - inputBoolean := NewInputBoolean("input_boolean.test") + t.Parallel() + inputBoolean := hal.NewInputBoolean("input_boolean.test") err := inputBoolean.TurnOff() - assert.Equal(t, err, ErrEntityNotRegistered) + assert.Equal(t, err, hal.ErrEntityNotRegistered) }) -} \ No newline at end of file +} diff --git a/entity_light_test.go b/entity_light_test.go index ce174b2..098fa82 100644 --- a/entity_light_test.go +++ b/entity_light_test.go @@ -1,20 +1,24 @@ -package hal +package hal_test import ( "testing" + "github.com/dansimau/hal" "github.com/dansimau/hal/homeassistant" "gotest.tools/v3/assert" ) func TestNewLight(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") assert.Equal(t, light.GetID(), "light.test") } func TestLight_GetBrightness(t *testing.T) { + t.Parallel() t.Run("returns brightness from attributes", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") state := homeassistant.State{ Attributes: map[string]any{ "brightness": float64(255), @@ -27,7 +31,8 @@ func TestLight_GetBrightness(t *testing.T) { }) t.Run("returns 0 when brightness not in attributes", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") state := homeassistant.State{ Attributes: map[string]any{}, } @@ -38,7 +43,8 @@ func TestLight_GetBrightness(t *testing.T) { }) t.Run("returns 0 when brightness is wrong type", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") state := homeassistant.State{ Attributes: map[string]any{ "brightness": "invalid", @@ -52,8 +58,10 @@ func TestLight_GetBrightness(t *testing.T) { } func TestLight_IsOn(t *testing.T) { + t.Parallel() t.Run("returns true when state is on", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") state := homeassistant.State{State: "on"} light.SetState(state) @@ -61,7 +69,8 @@ func TestLight_IsOn(t *testing.T) { }) t.Run("returns false when state is off", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") state := homeassistant.State{State: "off"} light.SetState(state) @@ -69,7 +78,8 @@ func TestLight_IsOn(t *testing.T) { }) t.Run("returns false when state is other value", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") state := homeassistant.State{State: "unavailable"} light.SetState(state) @@ -78,90 +88,106 @@ func TestLight_IsOn(t *testing.T) { } func TestLight_TurnOn(t *testing.T) { + t.Parallel() t.Run("returns error when not registered", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") err := light.TurnOn() - assert.Equal(t, err, ErrEntityNotRegistered) + assert.Equal(t, err, hal.ErrEntityNotRegistered) }) t.Run("calls service with attributes", func(t *testing.T) { + t.Parallel() // This would require mocking the connection and CallService method // For now, we'll test the error case above - light := NewLight("light.test") - + light := hal.NewLight("light.test") + // Test with attributes err := light.TurnOn(map[string]any{"brightness": 128}) - assert.Equal(t, err, ErrEntityNotRegistered) + assert.Equal(t, err, hal.ErrEntityNotRegistered) }) } func TestLight_TurnOff(t *testing.T) { + t.Parallel() t.Run("returns error when not registered", func(t *testing.T) { - light := NewLight("light.test") + t.Parallel() + light := hal.NewLight("light.test") err := light.TurnOff() - assert.Equal(t, err, ErrEntityNotRegistered) + assert.Equal(t, err, hal.ErrEntityNotRegistered) }) } func TestLightGroup_GetID(t *testing.T) { + t.Parallel() t.Run("returns empty group message for empty group", func(t *testing.T) { - lg := LightGroup{} + t.Parallel() + lg := hal.LightGroup{} assert.Equal(t, lg.GetID(), "(empty light group)") }) t.Run("returns joined IDs for multiple lights", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") - lg := LightGroup{light1, light2} + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") + lg := hal.LightGroup{light1, light2} assert.Equal(t, lg.GetID(), "light.1, light.2") }) } func TestLightGroup_GetBrightness(t *testing.T) { + t.Parallel() t.Run("returns 0 for empty group", func(t *testing.T) { - lg := LightGroup{} + t.Parallel() + lg := hal.LightGroup{} assert.Equal(t, lg.GetBrightness(), float64(0)) }) t.Run("returns brightness of first light", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") state1 := homeassistant.State{Attributes: map[string]any{"brightness": float64(100)}} state2 := homeassistant.State{Attributes: map[string]any{"brightness": float64(200)}} - + light1.SetState(state1) light2.SetState(state2) - lg := LightGroup{light1, light2} + lg := hal.LightGroup{light1, light2} assert.Equal(t, lg.GetBrightness(), float64(100)) }) } func TestLightGroup_GetState(t *testing.T) { + t.Parallel() t.Run("returns empty state for empty group", func(t *testing.T) { - lg := LightGroup{} + t.Parallel() + lg := hal.LightGroup{} state := lg.GetState() assert.DeepEqual(t, state, homeassistant.State{}) }) t.Run("returns state of first light", func(t *testing.T) { - light1 := NewLight("light.1") + t.Parallel() + light1 := hal.NewLight("light.1") expectedState := homeassistant.State{State: "on", Attributes: map[string]any{"brightness": float64(255)}} light1.SetState(expectedState) - lg := LightGroup{light1} + lg := hal.LightGroup{light1} state := lg.GetState() assert.DeepEqual(t, state, expectedState) }) } func TestLightGroup_SetState(t *testing.T) { + t.Parallel() t.Run("sets state on all lights in group", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") - lg := LightGroup{light1, light2} + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") + lg := hal.LightGroup{light1, light2} newState := homeassistant.State{State: "on", Attributes: map[string]any{"brightness": float64(128)}} lg.SetState(newState) @@ -172,48 +198,55 @@ func TestLightGroup_SetState(t *testing.T) { } func TestLightGroup_IsOn(t *testing.T) { + t.Parallel() t.Run("returns true when all lights are on", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") light1.SetState(homeassistant.State{State: "on"}) light2.SetState(homeassistant.State{State: "on"}) - lg := LightGroup{light1, light2} + lg := hal.LightGroup{light1, light2} assert.Equal(t, lg.IsOn(), true) }) t.Run("returns false when any light is off", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") light1.SetState(homeassistant.State{State: "on"}) light2.SetState(homeassistant.State{State: "off"}) - lg := LightGroup{light1, light2} + lg := hal.LightGroup{light1, light2} assert.Equal(t, lg.IsOn(), false) }) t.Run("returns true for empty group", func(t *testing.T) { - lg := LightGroup{} + t.Parallel() + lg := hal.LightGroup{} assert.Equal(t, lg.IsOn(), true) }) } func TestLightGroup_TurnOn(t *testing.T) { + t.Parallel() t.Run("returns nil when no errors", func(t *testing.T) { + t.Parallel() // Empty group should not error - lg := LightGroup{} + lg := hal.LightGroup{} err := lg.TurnOn() assert.NilError(t, err) }) t.Run("collects errors from individual lights", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") - lg := LightGroup{light1, light2} + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") + lg := hal.LightGroup{light1, light2} - // Both lights should return ErrEntityNotRegistered + // Both lights should return hal.ErrEntityNotRegistered err := lg.TurnOn() // With 2 errors, should return a joined error assert.ErrorContains(t, err, "entity not registered") @@ -221,16 +254,19 @@ func TestLightGroup_TurnOn(t *testing.T) { } func TestLightGroup_TurnOff(t *testing.T) { + t.Parallel() t.Run("returns nil when no errors", func(t *testing.T) { - lg := LightGroup{} + t.Parallel() + lg := hal.LightGroup{} err := lg.TurnOff() assert.NilError(t, err) }) t.Run("collects errors from individual lights", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") - lg := LightGroup{light1, light2} + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") + lg := hal.LightGroup{light1, light2} err := lg.TurnOff() // With 2 errors, should return joined error @@ -239,17 +275,19 @@ func TestLightGroup_TurnOff(t *testing.T) { } func TestLightGroup_BindConnection(t *testing.T) { + t.Parallel() t.Run("binds connection to all lights", func(t *testing.T) { - light1 := NewLight("light.1") - light2 := NewLight("light.2") - lg := LightGroup{light1, light2} + t.Parallel() + light1 := hal.NewLight("light.1") + light2 := hal.NewLight("light.2") + lg := hal.LightGroup{light1, light2} // Create a mock connection (would need proper setup in real scenario) - conn := &Connection{} + conn := &hal.Connection{} lg.BindConnection(conn) // Verify connection was bound (in real test, would check that connection property was set) // For now, just verify the method doesn't panic assert.Equal(t, len(lg), 2) }) -} \ No newline at end of file +} diff --git a/store/sqlite_test.go b/store/sqlite_test.go index bcd7cfb..0961008 100644 --- a/store/sqlite_test.go +++ b/store/sqlite_test.go @@ -1,40 +1,44 @@ -package store +package store_test import ( "os" "testing" + + "github.com/dansimau/hal/store" ) func TestSQLitePragmaConfiguration(t *testing.T) { + t.Parallel() + // Create a temporary database file tmpFile := "test_sqlite.db" defer os.Remove(tmpFile) - + // Test opening database with PRAGMA settings - db, err := Open(tmpFile) + db, err := store.Open(tmpFile) if err != nil { t.Fatalf("Failed to open database: %v", err) } - + // Verify WAL mode is set var journalMode string err = db.Raw("PRAGMA journal_mode").Scan(&journalMode).Error if err != nil { t.Fatalf("Failed to query journal_mode: %v", err) } - + if journalMode != "wal" { t.Errorf("Expected journal_mode to be 'wal', got: %s", journalMode) } - + // Verify synchronous mode is set var syncMode string err = db.Raw("PRAGMA synchronous").Scan(&syncMode).Error if err != nil { t.Fatalf("Failed to query synchronous: %v", err) } - + if syncMode != "1" { // NORMAL = 1 t.Errorf("Expected synchronous to be '1' (NORMAL), got: %s", syncMode) } -} \ No newline at end of file +} diff --git a/sun_internal_test.go b/sun_internal_test.go new file mode 100644 index 0000000..d951f0a --- /dev/null +++ b/sun_internal_test.go @@ -0,0 +1,21 @@ +package hal + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestNewSunTimes(t *testing.T) { + t.Parallel() + + config := LocationConfig{ + Latitude: 37.7749, + Longitude: -122.4194, + } + + sunTimes := NewSunTimes(config) + assert.Assert(t, sunTimes != nil) + assert.Equal(t, sunTimes.location.Latitude, 37.7749) + assert.Equal(t, sunTimes.location.Longitude, -122.4194) +} diff --git a/sun_test.go b/sun_test.go index e1f0bbd..b426241 100644 --- a/sun_test.go +++ b/sun_test.go @@ -1,46 +1,37 @@ -package hal +package hal_test import ( "testing" + "github.com/dansimau/hal" "gotest.tools/v3/assert" ) - -func TestNewSunTimes(t *testing.T) { - config := LocationConfig{ - Latitude: 37.7749, - Longitude: -122.4194, - } - - sunTimes := NewSunTimes(config) - assert.Assert(t, sunTimes != nil) - assert.Equal(t, sunTimes.location.Latitude, 37.7749) - assert.Equal(t, sunTimes.location.Longitude, -122.4194) -} - func TestSunTimes_Sunrise(t *testing.T) { + t.Parallel() t.Run("returns sunrise time for San Francisco", func(t *testing.T) { - config := LocationConfig{ + t.Parallel() + config := hal.LocationConfig{ Latitude: 37.7749, Longitude: -122.4194, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) sunrise := sunTimes.Sunrise() // Sunrise should be a valid time and in the past or future of today assert.Assert(t, !sunrise.IsZero()) - + // Sunrise should be before sunset sunset := sunTimes.Sunset() assert.Assert(t, sunrise.Before(sunset)) }) t.Run("returns different times for different locations", func(t *testing.T) { - sfConfig := LocationConfig{Latitude: 37.7749, Longitude: -122.4194} // San Francisco - nyConfig := LocationConfig{Latitude: 40.7128, Longitude: -74.0060} // New York + t.Parallel() + sfConfig := hal.LocationConfig{Latitude: 37.7749, Longitude: -122.4194} // San Francisco + nyConfig := hal.LocationConfig{Latitude: 40.7128, Longitude: -74.0060} // New York - sfSunTimes := NewSunTimes(sfConfig) - nySunTimes := NewSunTimes(nyConfig) + sfSunTimes := hal.NewSunTimes(sfConfig) + nySunTimes := hal.NewSunTimes(nyConfig) sfSunrise := sfSunTimes.Sunrise() nySunrise := nySunTimes.Sunrise() @@ -51,17 +42,19 @@ func TestSunTimes_Sunrise(t *testing.T) { } func TestSunTimes_Sunset(t *testing.T) { + t.Parallel() t.Run("returns sunset time for San Francisco", func(t *testing.T) { - config := LocationConfig{ + t.Parallel() + config := hal.LocationConfig{ Latitude: 37.7749, Longitude: -122.4194, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) sunset := sunTimes.Sunset() // Sunset should be a valid time assert.Assert(t, !sunset.IsZero()) - + // Sunset should be after sunrise sunrise := sunTimes.Sunrise() assert.Assert(t, sunset.After(sunrise)) @@ -69,12 +62,14 @@ func TestSunTimes_Sunset(t *testing.T) { } func TestSunTimes_IsDayTime(t *testing.T) { + t.Parallel() t.Run("correctly determines day/night for known time", func(t *testing.T) { - config := LocationConfig{ + t.Parallel() + config := hal.LocationConfig{ Latitude: 37.7749, Longitude: -122.4194, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) // This is a basic test - in real usage, the result depends on current time // We just verify the method returns a boolean and doesn't panic @@ -83,11 +78,12 @@ func TestSunTimes_IsDayTime(t *testing.T) { }) t.Run("day/night are opposite", func(t *testing.T) { - config := LocationConfig{ + t.Parallel() + config := hal.LocationConfig{ Latitude: 37.7749, Longitude: -122.4194, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) isDayTime := sunTimes.IsDayTime() isNightTime := sunTimes.IsNightTime() @@ -98,12 +94,14 @@ func TestSunTimes_IsDayTime(t *testing.T) { } func TestSunTimes_IsNightTime(t *testing.T) { + t.Parallel() t.Run("is opposite of IsDayTime", func(t *testing.T) { - config := LocationConfig{ + t.Parallel() + config := hal.LocationConfig{ Latitude: 37.7749, Longitude: -122.4194, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) isDayTime := sunTimes.IsDayTime() isNightTime := sunTimes.IsNightTime() @@ -114,13 +112,15 @@ func TestSunTimes_IsNightTime(t *testing.T) { } func TestSunTimes_EdgeCases(t *testing.T) { + t.Parallel() t.Run("handles extreme northern latitude", func(t *testing.T) { + t.Parallel() // Test with extreme latitude (northern Norway) - config := LocationConfig{ + config := hal.LocationConfig{ Latitude: 78.2156, Longitude: 15.5503, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) // Should not panic, even in polar regions sunrise := sunTimes.Sunrise() @@ -140,11 +140,12 @@ func TestSunTimes_EdgeCases(t *testing.T) { }) t.Run("handles zero coordinates", func(t *testing.T) { - config := LocationConfig{ + t.Parallel() + config := hal.LocationConfig{ Latitude: 0, Longitude: 0, } - sunTimes := NewSunTimes(config) + sunTimes := hal.NewSunTimes(config) // Should work at equator/prime meridian sunrise := sunTimes.Sunrise() @@ -154,4 +155,4 @@ func TestSunTimes_EdgeCases(t *testing.T) { assert.Assert(t, !sunset.IsZero()) assert.Assert(t, sunrise.Before(sunset)) }) -} \ No newline at end of file +} diff --git a/util_internal_test.go b/util_internal_test.go index 58eb47b..5cb4a14 100644 --- a/util_internal_test.go +++ b/util_internal_test.go @@ -2,6 +2,8 @@ package hal import ( "testing" + + "gotest.tools/v3/assert" ) func TestGetStringOrStringSlice(t *testing.T) { @@ -71,3 +73,51 @@ func TestGetStringOrStringSlice(t *testing.T) { }) } } + +func TestGetShortFunctionName(t *testing.T) { + t.Parallel() + + t.Run("extracts function name from function reference", func(t *testing.T) { + t.Parallel() + testFunc := func() {} + name := getShortFunctionName(testFunc) + + // The name should contain "TestGetShortFunctionName.func1" or similar + // depending on Go version and compilation + assert.Assert(t, len(name) > 0) + assert.Assert(t, name != "") + }) + + t.Run("handles named function", func(t *testing.T) { + t.Parallel() + name := getShortFunctionName(getShortFunctionName) + assert.Equal(t, name, "getShortFunctionName") + }) + + t.Run("handles method", func(t *testing.T) { + t.Parallel() + light := NewLight("test") + name := getShortFunctionName(light.TurnOn) + // Method names may have "-fm" suffix in Go runtime + assert.Assert(t, name == "TurnOn" || name == "TurnOn-fm") + }) +} + +func TestGetStringOrStringSliceAdditional(t *testing.T) { + t.Parallel() + + t.Run("handles boolean input", func(t *testing.T) { + t.Parallel() + result := getStringOrStringSlice(true) + assert.DeepEqual(t, result, []string{}) + }) + + t.Run("handles struct input", func(t *testing.T) { + t.Parallel() + type testStruct struct { + field string + } + result := getStringOrStringSlice(testStruct{field: "test"}) + assert.DeepEqual(t, result, []string{}) + }) +} diff --git a/util_test.go b/util_test.go deleted file mode 100644 index 1177b9b..0000000 --- a/util_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package hal - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func TestGetShortFunctionName(t *testing.T) { - t.Run("extracts function name from function reference", func(t *testing.T) { - testFunc := func() {} - name := getShortFunctionName(testFunc) - - // The name should contain "TestGetShortFunctionName.func1" or similar - // depending on Go version and compilation - assert.Assert(t, len(name) > 0) - assert.Assert(t, name != "") - }) - - t.Run("handles named function", func(t *testing.T) { - name := getShortFunctionName(getShortFunctionName) - assert.Equal(t, name, "getShortFunctionName") - }) - - t.Run("handles method", func(t *testing.T) { - light := NewLight("test") - name := getShortFunctionName(light.TurnOn) - // Method names may have "-fm" suffix in Go runtime - assert.Assert(t, name == "TurnOn" || name == "TurnOn-fm") - }) -} - -func TestGetStringOrStringSliceAdditional(t *testing.T) { - t.Run("handles boolean input", func(t *testing.T) { - result := getStringOrStringSlice(true) - assert.DeepEqual(t, result, []string{}) - }) - - t.Run("handles struct input", func(t *testing.T) { - type testStruct struct { - field string - } - result := getStringOrStringSlice(testStruct{field: "test"}) - assert.DeepEqual(t, result, []string{}) - }) -} \ No newline at end of file