diff --git a/phc/devicecontroller_mock.go b/phc/devicecontroller_mock.go new file mode 100644 index 00000000..83f0f29e --- /dev/null +++ b/phc/devicecontroller_mock.go @@ -0,0 +1,108 @@ +/* +Copyright (c) Facebook, Inc. and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: phc/phc.go + +// Package phc is a generated GoMock package. +package phc + +import ( + os "os" + reflect "reflect" + time "time" + + gomock "github.com/golang/mock/gomock" +) + +// MockDeviceController is a mock of DeviceController interface. +type MockDeviceController struct { + ctrl *gomock.Controller + recorder *MockDeviceControllerMockRecorder +} + +// MockDeviceControllerMockRecorder is the mock recorder for MockDeviceController. +type MockDeviceControllerMockRecorder struct { + mock *MockDeviceController +} + +// NewMockDeviceController creates a new mock instance. +func NewMockDeviceController(ctrl *gomock.Controller) *MockDeviceController { + mock := &MockDeviceController{ctrl: ctrl} + mock.recorder = &MockDeviceControllerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeviceController) EXPECT() *MockDeviceControllerMockRecorder { + return m.recorder +} + +// File mocks base method. +func (m *MockDeviceController) File() *os.File { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "File") + ret0, _ := ret[0].(*os.File) + return ret0 +} + +// File indicates an expected call of File. +func (mr *MockDeviceControllerMockRecorder) File() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "File", reflect.TypeOf((*MockDeviceController)(nil).File)) +} + +// Time mocks base method. +func (m *MockDeviceController) Time() (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Time") + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Time indicates an expected call of Time. +func (mr *MockDeviceControllerMockRecorder) Time() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Time", reflect.TypeOf((*MockDeviceController)(nil).Time)) +} + +// setPTPPerout mocks base method. +func (m *MockDeviceController) setPTPPerout(req PTPPeroutRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "setPTPPerout", req) + ret0, _ := ret[0].(error) + return ret0 +} + +// setPTPPerout indicates an expected call of setPTPPerout. +func (mr *MockDeviceControllerMockRecorder) setPTPPerout(req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setPTPPerout", reflect.TypeOf((*MockDeviceController)(nil).setPTPPerout), req) +} + +// setPinFunc mocks base method. +func (m *MockDeviceController) setPinFunc(index uint, pf PinFunc, ch uint) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "setPinFunc", index, pf, ch) + ret0, _ := ret[0].(error) + return ret0 +} + +// setPinFunc indicates an expected call of setPinFunc. +func (mr *MockDeviceControllerMockRecorder) setPinFunc(index, pf, ch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setPinFunc", reflect.TypeOf((*MockDeviceController)(nil).setPinFunc), index, pf, ch) +} diff --git a/phc/helper_386.go b/phc/helper_386.go new file mode 100644 index 00000000..a2e14f63 --- /dev/null +++ b/phc/helper_386.go @@ -0,0 +1,29 @@ +//go:build 386 && !darwin + +/* +Copyright (c) Facebook, Inc. and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package phc + +import ( + "time" + + "golang.org/x/sys/unix" +) + +func timeToTimespec(time time.Time) (ts unix.Timespec) { + return unix.Timespec{Sec: int32(time.Unix()), Nsec: int32(time.Nanosecond())} +} diff --git a/phc/helper_64bit.go b/phc/helper_64bit.go new file mode 100644 index 00000000..77327c12 --- /dev/null +++ b/phc/helper_64bit.go @@ -0,0 +1,29 @@ +//go:build !386 && !darwin + +/* +Copyright (c) Facebook, Inc. and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package phc + +import ( + "time" + + "golang.org/x/sys/unix" +) + +func timeToTimespec(time time.Time) (ts unix.Timespec) { + return unix.Timespec{Sec: time.Unix(), Nsec: int64(time.Nanosecond())} +} diff --git a/phc/phc.go b/phc/phc.go index 1d0f5e44..325fb80e 100644 --- a/phc/phc.go +++ b/phc/phc.go @@ -152,6 +152,23 @@ func Time(iface string, method TimeMethod) (time.Time, error) { } } +// PPSSource represents a PPS source +type PPSSource struct { + PHCDevice DeviceController + state PPSSourceState + peroutPhase int +} + +// PPSSourceState represents the state of a PPS source +type PPSSourceState int + +const ( + // UnknownStatus is the initial state of a PPS source, which means PPS may or may not be configured + UnknownStatus PPSSourceState = iota + // PPSSet means the underlying device is activated as a PPS source + PPSSet +) + // Device represents a PHC device type Device os.File @@ -306,7 +323,7 @@ func (dev *Device) AdjFreq(freqPPB float64) error { return clockAdjFreq(dev, fre func (dev *Device) Step(step time.Duration) error { return clockStep(dev, step) } // ActivatePPSSource configures the PHC device to be a PPS timestamp source -func ActivatePPSSource(dev DeviceController) error { +func ActivatePPSSource(dev DeviceController) (*PPSSource, error) { // Initialize the PTPPeroutRequest struct peroutRequest := PTPPeroutRequest{} @@ -319,7 +336,7 @@ func ActivatePPSSource(dev DeviceController) error { ts, err := dev.Time() if err != nil { - return fmt.Errorf("failed (clock_gettime) on %s", dev.File().Name()) + return nil, fmt.Errorf("failed (clock_gettime) on %s", dev.File().Name()) } // Set the index and period @@ -343,8 +360,50 @@ func ActivatePPSSource(dev DeviceController) error { log.Debugf("retrying PTP_PEROUT_REQUEST2 with DUTY_CYCLE flag unset for backwards compatibility") peroutRequest.Flags &^= ptpPeroutDutyCycle err = dev.setPTPPerout(peroutRequest) - return err + + if err != nil { + return nil, err + } } - return nil + return &PPSSource{PHCDevice: dev, state: PPSSet}, nil +} + +// Timestamp returns the timestamp of the last PPS output edge from the given PPS source +// A Pointer is returned to avoid additional memory allocation +func (ppsSource *PPSSource) Timestamp() (*time.Time, error) { + if ppsSource.state != PPSSet { + return nil, fmt.Errorf("PPS source not set") + } + + currTime, err := ppsSource.PHCDevice.Time() + + if err != nil { + return nil, fmt.Errorf("error getting time (clock_gettime) on %s", ppsSource.PHCDevice.File().Name()) + } + + // subtract device perout phase from current time to get the time of the last perout output edge + // TODO: optimize section below using binary operations instead of type conversions + currTime = currTime.Add(-time.Duration(ppsSource.peroutPhase)) + sourceTs := timeToTimespec(currTime) + + /* + * As long as the kernel doesn't support a proper API for reporting + * back a precise perout timestamp, we have to assume that the current + * time on the PPS source is still within +/- half a second of the last + * perout output edge, and hence, we can deduce the current second + * (nanossecond is omitted) of this edge at the emitter based on the + * emitter's current time. We support only PHC sources, so we can ignore + * the NMEA source edge case described in ts2phc.c + */ + //nolint:govet + if int64(sourceTs.Nsec) > int64(nsPerSec/2) { + sourceTs.Sec++ + sourceTs.Nsec = 0 + } + //nolint:govet + currTime = time.Unix(int64(sourceTs.Sec), int64(sourceTs.Nsec)) + currTime = currTime.Add(time.Duration(ppsSource.peroutPhase)) + + return &currTime, nil } diff --git a/phc/phc_test.go b/phc/phc_test.go index 265bd8d9..9d24a59c 100644 --- a/phc/phc_test.go +++ b/phc/phc_test.go @@ -22,34 +22,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/mock" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" ) -type deviceControllerMock struct { - mock.Mock -} - -func (_m *deviceControllerMock) Time() (time.Time, error) { - ret := _m.Called() - return ret.Get(0).(time.Time), ret.Error(1) -} - -func (_m *deviceControllerMock) setPinFunc(index uint, pf PinFunc, ch uint) error { - ret := _m.Called(index, pf, ch) - return ret.Error(0) -} - -func (_m *deviceControllerMock) setPTPPerout(req PTPPeroutRequest) error { - ret := _m.Called(req) - return ret.Error(0) -} - -func (_m *deviceControllerMock) File() *os.File { - ret := _m.Called() - return ret.Get(0).(*os.File) -} - func TestIfaceInfoToPHCDevice(t *testing.T) { info := &EthtoolTSinfo{ PHCIndex: 0, @@ -83,91 +60,158 @@ func TestMaxAdjFreq(t *testing.T) { func TestActivatePPSSource(t *testing.T) { // Prepare - mockDevice := new(deviceControllerMock) - - // Should set default pin to PPS - mockDevice.On("setPinFunc", uint(0), PinFuncPerOut, uint(0)).Return(nil).Once() - - // Should call Time once - mockDevice.On("Time").Return(time.Unix(824635825488, 1397965136), nil).Once() - - // Should issue ioctlPTPPeroutRequest2 + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + var actualPeroutRequest PTPPeroutRequest + gomock.InOrder( + // Should set default pin to PPS + mockDeviceController.EXPECT().setPinFunc(uint(0), PinFuncPerOut, uint(0)).Return(nil), + // Should call Time once + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil), + mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(nil).Do(func(arg PTPPeroutRequest) { actualPeroutRequest = arg }), + ) + + // Should call setPTPPerout with correct parameters expectedPeroutRequest := PTPPeroutRequest{ Index: uint32(0), - Flags: uint32(0x2), - StartOrPhase: PTPClockTime{Sec: 51}, + Flags: uint32(2), + StartOrPhase: PTPClockTime{Sec: 2}, Period: PTPClockTime{Sec: 1}, On: PTPClockTime{NSec: 500000000}, } - mockDevice.On("setPTPPerout", expectedPeroutRequest).Return(nil).Once() // Act - err := ActivatePPSSource(mockDevice) + ppsSource, err := ActivatePPSSource(mockDeviceController) - // Assert calls + // Assert require.NoError(t, err) - mockDevice.AssertExpectations(t) + require.EqualValues(t, expectedPeroutRequest, actualPeroutRequest, "setPTPPerout parameter mismatch") + require.Equal(t, PPSSet, ppsSource.state) } func TestActivatePPSSourceIgnoreSetPinFailure(t *testing.T) { // Prepare - mockDevice := new(deviceControllerMock) - mockDevice.On("File").Return(os.NewFile(3, "mock_file")) - mockDevice.On("Time").Return(time.Unix(824635825488, 1397965136), nil) - - // If ioctl set pin fails, we continue bravely on... - mockDevice.On("setPinFunc", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("error")).Once() - mockDevice.On("setPTPPerout", mock.Anything).Return(nil).Once() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + gomock.InOrder( + // If ioctl set pin fails, we continue bravely on... + mockDeviceController.EXPECT().setPinFunc(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")), + mockDeviceController.EXPECT().File().Return(os.NewFile(3, "mock_file")), + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil), + mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(nil), + ) // Act - err := ActivatePPSSource(mockDevice) + ppsSource, err := ActivatePPSSource(mockDeviceController) - // Assert calls - mockDevice.AssertExpectations(t) + // Assert require.NoError(t, err) + require.Equal(t, PPSSet, ppsSource.state) } func TestActivatePPSSourceSetPTPPeroutFailure(t *testing.T) { // Prepare - mockDevice := new(deviceControllerMock) - mockDevice.On("File").Return(os.NewFile(3, "mock_file")) - mockDevice.On("Time").Return(time.Unix(824635825488, 1397965136), nil) - - mockDevice.On("setPinFunc", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("error")).Once() - // If first attempt to set PTPPerout fails - mockDevice.On("setPTPPerout", mock.Anything).Return(fmt.Errorf("error")).Once() - - // Should retry setPTPPerout with backward compatible flag + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + var actualPeroutRequest PTPPeroutRequest + gomock.InOrder( + mockDeviceController.EXPECT().setPinFunc(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")), + mockDeviceController.EXPECT().File().Return(os.NewFile(3, "mock_file")), + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil), + // If first attempt to set PTPPerout fails + mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(fmt.Errorf("error")), + // Should retry setPTPPerout with backward compatible flag + mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(nil).Do(func(arg PTPPeroutRequest) { actualPeroutRequest = arg }), + ) expectedPeroutRequest := PTPPeroutRequest{ Index: uint32(0), Flags: uint32(0x0), - StartOrPhase: PTPClockTime{Sec: 51}, + StartOrPhase: PTPClockTime{Sec: 2}, Period: PTPClockTime{Sec: 1}, On: PTPClockTime{NSec: 500000000}, } - mockDevice.On("setPTPPerout", expectedPeroutRequest).Return(nil).Once() // Act - err := ActivatePPSSource(mockDevice) + ppsSource, err := ActivatePPSSource(mockDeviceController) // Assert - mockDevice.AssertExpectations(t) require.NoError(t, err) + require.EqualValues(t, expectedPeroutRequest, actualPeroutRequest, "setPTPPerout parameter mismatch") + require.Equal(t, PPSSet, ppsSource.state) } func TestActivatePPSSourceSetPTPPeroutDoubleFailure(t *testing.T) { // Prepare - mockDevice := new(deviceControllerMock) - mockDevice.On("File").Return(os.NewFile(3, "mock_file")) - mockDevice.On("Time").Return(time.Unix(824635825488, 1397965136), nil) - mockDevice.On("setPinFunc", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("error")).Once() - mockDevice.On("setPTPPerout", mock.Anything).Return(fmt.Errorf("error")).Once() - mockDevice.On("setPTPPerout", mock.Anything).Return(fmt.Errorf("error")).Once() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + gomock.InOrder( + mockDeviceController.EXPECT().setPinFunc(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")), + mockDeviceController.EXPECT().File().Return(os.NewFile(3, "mock_file")), + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500000000), nil), + mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(fmt.Errorf("error")), + mockDeviceController.EXPECT().setPTPPerout(gomock.Any()).Return(fmt.Errorf("error")), + ) + + // Act + ppsSource, err := ActivatePPSSource(mockDeviceController) + + // Assert + require.Error(t, err) + require.Nil(t, ppsSource) +} + +func TestGetPPSTimestampSourceUnset(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + ppsSource := PPSSource{PHCDevice: mockDeviceController} // Act - err := ActivatePPSSource(mockDevice) + _, err := ppsSource.Timestamp() // Assert - mockDevice.AssertExpectations(t) require.Error(t, err) } + +func TestGetPPSTimestampMoreThanHalfNanossecondShouldAddSecond(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + ppsSource := PPSSource{PHCDevice: mockDeviceController, state: PPSSet, peroutPhase: 23312} + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500023313), nil) + + // Act + timestamp, err := ppsSource.Timestamp() + + // Assert + expected := time.Unix(1075896001, 23312) + require.NoError(t, err) + require.EqualValues(t, expected, *timestamp) +} + +func TestGetPPSTimestampLessThanHalfNanossecondShouldKeepNanosseconds(t *testing.T) { + // Prepare + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDeviceController := NewMockDeviceController(ctrl) + ppsSource := PPSSource{PHCDevice: mockDeviceController, state: PPSSet, peroutPhase: 23312} + mockDeviceController.EXPECT().Time().Return(time.Unix(1075896000, 500023312), nil) + + // Act + timestamp, err := ppsSource.Timestamp() + + // Assert + expected := time.Unix(1075896000, 500023312) + require.NoError(t, err) + require.EqualValues(t, expected, *timestamp) +} + +func TestTimeToTimespec(t *testing.T) { + someTime := time.Unix(1075896000, 500000000) + result := timeToTimespec(someTime) + require.Equal(t, result, unix.Timespec{Sec: 1075896000, Nsec: 500000000}) +}