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
23 changes: 17 additions & 6 deletions resources/statefulsensor/statefulsensor.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ type statefulSensor struct {
resource.AlwaysRebuild
resource.Named

name resource.Name
logger logging.Logger
cfg *Config

Expand Down Expand Up @@ -91,7 +90,7 @@ func New(ctx context.Context, deps resource.Dependencies, name resource.Name, co
}

s := &statefulSensor{
name: name,
Named: name.AsNamed(),
logger: logger,
cfg: conf,
filePath: filePath,
Expand Down Expand Up @@ -169,10 +168,6 @@ func (s *statefulSensor) saveToFile() error {
return nil
}

func (s *statefulSensor) Name() resource.Name {
return s.name
}

func (s *statefulSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
s.mu.RLock()
defer s.mu.RUnlock()
Expand All @@ -185,6 +180,22 @@ func (s *statefulSensor) Readings(ctx context.Context, extra map[string]interfac
return readings, nil
}

// Status reports operational metadata for the sensor, served via the sensor
// service's GetStatus RPC. It is defined explicitly (rather than relying on the
// empty default promoted from the embedded resource.Named) so the model both
// exposes useful state and is guaranteed to have a non-nil receiver. Unlike
// Readings, which returns the stored value itself, Status reports where the
// value is persisted and how many keys it holds.
func (s *statefulSensor) Status(ctx context.Context) (map[string]interface{}, error) {
s.mu.RLock()
defer s.mu.RUnlock()

return map[string]interface{}{
"file_path": s.filePath,
"num_keys": len(s.value),
}, nil
}

// DoCommand supports a "set" command that replaces the value the sensor holds.
//
// Example:
Expand Down
34 changes: 34 additions & 0 deletions resources/statefulsensor/statefulsensor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,40 @@ func TestDefaultPathUsesModuleDataDir(t *testing.T) {
}
}

// TestStatus verifies the explicit Status API. It also guards against a
// regression where the embedded resource.Named was left uninitialized: the
// sensor service's GetStatus RPC calls Status on the resource, and with a nil
// embedded Named the promoted default Status dereferenced a nil receiver and
// crashed the module (SIGSEGV).
func TestStatus(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "state.json")
s := newTestSensor(t, path)

status, err := s.Status(ctx)
if err != nil {
t.Fatalf("Status: %v", err)
}
if status["file_path"] != path {
t.Fatalf("Status file_path = %v, want %q", status["file_path"], path)
}
if status["num_keys"] != 0 {
t.Fatalf("Status num_keys = %v, want 0", status["num_keys"])
}

if _, err := s.DoCommand(ctx, map[string]interface{}{"set": map[string]interface{}{"a": 1.0, "b": 2.0}}); err != nil {
t.Fatalf("DoCommand set: %v", err)
}

status, err = s.Status(ctx)
if err != nil {
t.Fatalf("Status after set: %v", err)
}
if status["num_keys"] != 2 {
t.Fatalf("Status num_keys = %v, want 2", status["num_keys"])
}
}

func TestDoCommandRejectsUnknownCommand(t *testing.T) {
ctx := context.Background()
s := newTestSensor(t, filepath.Join(t.TempDir(), "state.json"))
Expand Down