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
121 changes: 84 additions & 37 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,61 +14,108 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Code
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Setup Pixi
- name: Setup pixi
uses: prefix-dev/setup-pixi@8ca4608ef7f4daeb54f5205b20d0b7cb42f11143 # v0.8.14
with:
pixi-version: v0.56.0
cache: false
frozen: true

- name: Run Linting Checks
- name: Run linting checks
run: pixi run lint

prepare-image:
name: Prepare EPICS Container Image
build-and-test:
name: Build and Test

needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
rhel_version: [9, 10]
container:
image: ghcr.io/nsls2/epics-alma${{ matrix.rhel_version }}:latest
options: --user root

steps:
- name: Login to GitHub Container Registry
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Create CONFIG_SITE.local
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u "${{ github.actor }}" --password-stdin
cat > configure/CONFIG_SITE.local << EOF
# CONFIG_SITE.local for GitHub Actions CI
CHECK_RELEASE=NO
BUILD_IOCS=YES
BUILD_TESTS=YES

WITH_BOOST=NO
WITH_PVA=YES
WITH_QSRV=YES
WITH_BLOSC=YES
BLOSC_EXTERNAL=YES
WITH_BITSHUFFLE=NO
WITH_GRAPHICSMAGICK=NO
WITH_HDF5=YES
HDF5_EXTERNAL=YES
WITH_JSON=YES
WITH_JPEG=YES
JPEG_EXTERNAL=YES
WITH_NETCDF=NO
WITH_NEXUS=NO
WITH_OPENCV=NO
WITH_SZIP=YES
SZIP_EXTERNAL=YES
WITH_TIFF=YES
TIFF_EXTERNAL=YES
XML2_EXTERNAL=YES
WITH_ZLIB=YES
ZLIB_EXTERNAL=YES
EOF

echo "CONFIG_SITE.local created:"
cat configure/CONFIG_SITE.local

- name: Get Container Digest
id: digest
- name: Create RELEASE.local
run: |
set -e
DIGEST=$(docker manifest inspect \
ghcr.io/nsls2/epics-alma8:latest --verbose \
| jq -r '.[0].Descriptor.digest') || {
echo "Failed to retrieve container digest" >&2
exit 1
}
if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then
echo "Invalid container digest: $DIGEST" >&2
exit 1
fi
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
echo "Container digest: $DIGEST"

- name: Cache Docker Image
id: cache
uses: actions/cache@v4
with:
path: /tmp/epics-alma8.tar
key: epics-alma8-${{ steps.digest.outputs.digest }}
cat > configure/RELEASE.local << EOF
# RELEASE.local for GitHub Actions CI
ADSUPPORT=/usr/lib64/epics
ADCORE=/usr/lib64/epics
ASYN=/usr/lib64/epics
AUTOSAVE=/usr/lib64/epics
BUSY=/usr/lib64/epics
CALC=/usr/lib64/epics
DEVIOCSTATS=/usr/lib64/epics
SSCAN=/usr/lib64/epics
STREAM=/usr/lib64/epics
SNCSEQ=/usr/lib64/epics
RECCASTER=/usr/lib64/epics
MOTOR=/usr/lib64/epics

# EPICS_BASE should always be last
EPICS_BASE=/usr/lib64/epics

EOF

- name: Pull and Save Container Image
if: steps.cache.outputs.cache-hit != 'true'
echo "RELEASE.local created:"
cat configure/RELEASE.local

# TODO: Check why the non-devel deps are not being pulled in
# Epics bundle rpm should be converted to require devel versions of packages
- name: Install required system packages
run: |
echo "Pulling fresh image..."
docker pull ghcr.io/nsls2/epics-alma8:latest
docker save ghcr.io/nsls2/epics-alma8:latest -o /tmp/epics-alma8.tar
dnf -y install curl-devel zeromq-devel zlib-devel blosc-devel hdf5-devel libtiff-devel libjpeg-devel libaec-devel libxml2-devel

build:
- name: Build ADXSPD driver and unit tests
run: |
make -j4

test:
- name: Run unit tests
run: |
./bin/linux-x86_64/TestADXSPD
2 changes: 1 addition & 1 deletion iocs/xspdIOC/xspdApp/src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ else
PROD_SYS_LIBS += zmq
endif

PROD_SYS_LIBS += curl z
PROD_SYS_LIBS += curl

include $(ADCORE)/ADApp/commonDriverMakefile

Expand Down
17 changes: 8 additions & 9 deletions xspdApp/src/XSPDAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ string XSPD::API::GetDeviceAtIndex(int deviceIndex) {
* @return Pointer to the initialized Detector object
*/
XSPD::Detector* XSPD::API::Initialize(string deviceId) {

// Get API version information
json apiVersionInfo = SubmitRequest(this->baseUri + "/api", XSPD::RequestType::GET);

Expand All @@ -77,8 +76,8 @@ XSPD::Detector* XSPD::API::Initialize(string deviceId) {

// Retrieve detector information
json deviceInfo;
try{
deviceInfo = GetVar<json>("info");
try {
deviceInfo = GetVar<json>("info");
} catch (out_of_range& e) {
throw runtime_error("Failed to retrieve device info for device ID " + this->deviceId +
": " + string(e.what()));
Expand Down Expand Up @@ -114,13 +113,13 @@ XSPD::Detector* XSPD::API::Initialize(string deviceId) {
throw runtime_error("No data ports found for device ID " + this->deviceId);

for (auto& dpInfo : dataPortInfo) {
if(!dpInfo.contains("id") || !dpInfo.contains("ip") || !dpInfo.contains("port"))
throw runtime_error("Data port information is missing 'id', 'ip', or 'port' field for device ID " +
this->deviceId);
if (!dpInfo.contains("id") || !dpInfo.contains("ip") || !dpInfo.contains("port"))
throw runtime_error(
"Data port information is missing 'id', 'ip', or 'port' field for device ID " +
this->deviceId);

DataPort* pdataPort =
new DataPort(this, dpInfo["id"].get<string>(), dpInfo["ip"].get<string>(),
dpInfo["port"].get<int>());
DataPort* pdataPort = new DataPort(this, dpInfo["id"].get<string>(),
dpInfo["ip"].get<string>(), dpInfo["port"].get<int>());
this->detector->RegisterDataPort(pdataPort);
}

Expand Down
1 change: 0 additions & 1 deletion xspdApp/src/XSPDAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ class API {
Detector* Initialize(string deviceId = "");
virtual ~API() {}


void GetVersionInfo();
string GetXSPDVersion();
string GetLibXSPVersion();
Expand Down
2 changes: 1 addition & 1 deletion xspdApp/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ TestADXSPD_SRCS += MockXSPDAPI.cpp
# Add additional test source files here
# TestADXSPD_SRCS +=

TestADXSPD_LIBS += ADXSPD ADBase asyn cpr gtest gmock $(EPICS_BASE_IOC_LIBS) xml2
TestADXSPD_LIBS += ADXSPD ADBase asyn cpr gtest gmock $(EPICS_BASE_IOC_LIBS)

ifdef ZMQ_LIB
zmq_DIR += $(ZMQ_LIB)
Expand Down
31 changes: 16 additions & 15 deletions xspdApp/tests/MockXSPDAPI.cpp
Original file line number Diff line number Diff line change
@@ -1,64 +1,64 @@
#include "MockXSPDAPI.h"

#include <fstream>

using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Throw;
using ::testing::Invoke;
using ::testing::Return;
using ::testing::SetArgPointee;
using ::testing::Throw;

MockXSPDAPI::MockXSPDAPI() : XSPD::API("localhost", 8008) {
string sampleResponsesPath = "xspdApp/tests/samples/xspd_sample_resp_sim.json";
std::ifstream file(sampleResponsesPath);
if (!file.is_open())
throw std::runtime_error("Failed to open sample responses JSON file at " + sampleResponsesPath);
throw std::runtime_error("Failed to open sample responses JSON file at " +
sampleResponsesPath);

this->sampleResponses = json::parse(file);
// Add a second device
this->sampleResponses["localhost:8008/api/v1/devices"]["devices"].push_back({{"id", "device456"}});
this->sampleResponses["localhost:8008/api/v1/devices"]["devices"].push_back(
{{"id", "device456"}});
};

void MockXSPDAPI::MockAPIVerionCheck() {
string uri = "localhost:8008/api";
json response = this->sampleResponses[uri];
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET))
.WillOnce(Return(response));
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET)).WillOnce(Return(response));
std::cout << "Mocked GET request to URI: " << uri << std::endl;
std::cout << "Returning response: " << response.dump(4) << std::endl;
}

void MockXSPDAPI::MockGetRequest(string endpoint, json* alternateResponse) {
string uri = "localhost:8008/api/v1/" + endpoint;
if (!this->sampleResponses.contains(uri)){
if (!this->sampleResponses.contains(uri)) {
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET))
.WillOnce(Throw(std::runtime_error("Failed to get data from " + uri)));
std::cout << "Mocked GET request to URI: " << uri << std::endl;
std::cout << "Mocking non 200 response code." << std::endl;
} else {
json response = alternateResponse ? *alternateResponse : this->sampleResponses[uri];
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET))
.WillOnce(Return(response));
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET)).WillOnce(Return(response));
std::cout << "Mocked GET request to URI: " << uri << std::endl;
std::cout << "Returning response: " << response.dump(4) << std::endl;
}
}

void MockXSPDAPI::MockGetVarRequest(string variableEndpoint, json* alternateResponse) {
this->MockGetRequest("devices/lambda01/variables?path=" + variableEndpoint,
alternateResponse);
this->MockGetRequest("devices/lambda01/variables?path=" + variableEndpoint, alternateResponse);
}

void MockXSPDAPI::MockRepeatedGetRequest(string endpoint, json* alternateResponse) {
string uri = "localhost:8008/api/v1/" + endpoint;
json response = alternateResponse ? *alternateResponse : this->sampleResponses[uri];
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET))
.WillRepeatedly(Return(response));
EXPECT_CALL(*this, SubmitRequest(uri, XSPD::RequestType::GET)).WillRepeatedly(Return(response));
std::cout << "Mocked GET request to URI: " << uri << std::endl;
std::cout << "Returning response: " << response.dump(4) << std::endl;
}

// void MockXSPDAPI::MockIncompleteInitializationSeq(XSPD::APIState stopAtState, std::string deviceId) {
// void MockXSPDAPI::MockIncompleteInitializationSeq(XSPD::APIState stopAtState, std::string
// deviceId) {
// InSequence seq;
// switch (stopAtState) {
// case stopAtState < XSPD::APIState::CHECKING_API_VERSION:
Expand All @@ -74,7 +74,8 @@ void MockXSPDAPI::MockRepeatedGetRequest(string endpoint, json* alternateRespons
// this->MockGetRequest("devices/" + deviceId + "/variables?path=info");
// break;
// default:
// throw std::invalid_argument("Invalid stopAtState for MockIncompleteInitializationSeq");
// throw std::invalid_argument("Invalid stopAtState for
// MockIncompleteInitializationSeq");
// }
// }

Expand All @@ -100,4 +101,4 @@ void MockXSPDAPI::MockInitializationSeq(std::string deviceId) {
XSPD::Detector* MockXSPDAPI::MockInitialization(std::string deviceId) {
this->MockInitializationSeq(deviceId);
return this->Initialize(deviceId);
}
}
3 changes: 2 additions & 1 deletion xspdApp/tests/MockXSPDAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class MockXSPDAPI : public XSPD::API {
void MockGetVarRequest(string variableEndpoint, json* alternateResponse = nullptr);
void MockRepeatedGetRequest(string endpoint, json* alternateResponse = nullptr);
void MockInitializationSeq(string deviceId = "lambda01");
// void MockIncompleteInitializationSeq(XSPD::APIState stopAtState, std::string deviceId = "lambda01");
// void MockIncompleteInitializationSeq(XSPD::APIState stopAtState, std::string deviceId =
// "lambda01");
XSPD::Detector* MockInitialization(string deviceId = "lambda01");
void MockAPIVerionCheck();

Expand Down
2 changes: 1 addition & 1 deletion xspdApp/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

This directory contains unit tests written to cover both the general inteface to the xspd API classes (i.e. those in the `XSPD` namespace), as well as the `ADXSPD` areaDetector driver. The tests are written with [GoogleTest](https://github.com/google/googletest). To build the tests, add `BUILD_TESTS=YES` to the `CONFIG_SITE` or `CONFIG_SITE.local` file in the top level `configure` directory, and build the driver. The tests will be installed in `bin/$ARCH/TestADXSPD`.

The tests rely on example resonse data that is fed through a Mocked API interface class. The example data is produced by running the included `generate_sample_response_json` script while the X-Spectrum provided simulated detector is running (along with the xspd service).
The tests rely on example resonse data that is fed through a Mocked API interface class. The example data is produced by running the included `generate_sample_response_json` script while the X-Spectrum provided simulated detector is running (along with the xspd service).
1 change: 0 additions & 1 deletion xspdApp/tests/TestADXSPD.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ using ::testing::StrictMock;
class TestADXSPD : public ::testing::Test {
protected:
void SetUpTestSuite() {

// this->simulatedXSPDService = boost::process::child(".pixi/envs/default/bin/python3
// sim/xspdSimulator.py",
// boost::process::std_out > boost::process::null,
Expand Down
11 changes: 3 additions & 8 deletions xspdApp/tests/TestADXSPDMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
#include <gtest/gtest.h>

class PrintRequestsAndReturnsOnFailure : public ::testing::EmptyTestEventListener {

virtual void OnTestStart(const ::testing::TestInfo& /*test_info*/) override {
// Start capturing output
::testing::internal::CaptureStdout();
Expand All @@ -23,17 +22,14 @@ class PrintRequestsAndReturnsOnFailure : public ::testing::EmptyTestEventListene
std::string standardError = ::testing::internal::GetCapturedStderr();
if (test_info.result()->Failed()) {
if (!standardOutput.empty()) {
std::cout << "Captured Standard Output:\n"
<< standardOutput << std::endl;
std::cout << "Captured Standard Output:\n" << standardOutput << std::endl;
}

if (!standardError.empty()) {
std::cerr << "Captured Standard Error:\n"
<< standardError << std::endl;
std::cerr << "Captured Standard Error:\n" << standardError << std::endl;
}
}
}

};

// Main function to run tests
Expand All @@ -42,8 +38,7 @@ int main(int argc, char** argv) {
::testing::InitGoogleMock(&argc, argv);

// Instantiate our custom listener
::testing::TestEventListeners& listeners =
::testing::UnitTest::GetInstance()->listeners();
::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners();
listeners.Append(new PrintRequestsAndReturnsOnFailure());

return RUN_ALL_TESTS();
Expand Down
Loading