Skip to content

Commit ec878c1

Browse files
authored
Add code coverage (#11)
* add macos to build workflow * make C17 required * for osx build both archs * Add temp artifacts to builds * fix cmake.yml * Create setup.py * Update setup.py * Create coverage.yml * Update CMakeLists.txt * improve coverage * update coverage * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * add GAHealth tests
1 parent 209e005 commit ec878c1

File tree

5 files changed

+340
-4
lines changed

5 files changed

+340
-4
lines changed

.github/workflows/coverage.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Test Coverage
2+
3+
on: [pull_request, workflow_dispatch]
4+
5+
jobs:
6+
build:
7+
name: Report Test Coverage
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
with:
13+
submodules: true
14+
15+
- name: Create Build Environment
16+
run: cmake -E make_directory ${{github.workspace}}/build
17+
18+
- name: Setup LCOV
19+
uses: hrishikesh-kadam/setup-lcov@v1
20+
21+
- name: Configure CMake
22+
shell: bash
23+
working-directory: ${{github.workspace}}/build
24+
run: cmake ..
25+
26+
- name: Build
27+
working-directory: ${{github.workspace}}/build
28+
shell: bash
29+
run: cmake --build .
30+
31+
- name: Prepare coverage data
32+
working-directory: ${{github.workspace}}/build
33+
shell: bash
34+
run: cmake --build . --target cov_data
35+
36+
- name: Report code coverage
37+
uses: zgosalvez/github-actions-report-lcov@v3
38+
with:
39+
coverage-files: build/cov.info.cleaned
40+
minimum-coverage: 30
41+
artifact-name: code-coverage-report
42+
github-token: ${{ secrets.GITHUB_TOKEN }}
43+
working-directory: ${{github.workspace}}
44+
update-comment: true
45+

CMakeLists.txt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,84 @@ target_link_libraries(${UT_PROJECT_NAME} ${PROJECT_NAME})
245245

246246
add_test(NAME ${UT_PROJECT_NAME} COMMAND GameAnalyticsUnitTests)
247247

248+
# --------------------------- Google Test Setup --------------------------- #
249+
250+
find_program(GCOV_PATH gcov)
251+
if (NOT GCOV_PATH)
252+
message(WARNING "program gcov not found")
253+
endif()
254+
255+
find_program(LCOV_PATH lcov)
256+
if (NOT LCOV_PATH)
257+
message(WARNING "program lcov not found")
258+
endif()
259+
260+
find_program(GENHTML_PATH genhtml)
261+
if (NOT GENHTML_PATH)
262+
message(WARNING "program genhtml not found")
263+
endif()
264+
265+
if (LCOV_PATH AND GCOV_PATH)
266+
267+
target_compile_options(
268+
GameAnalytics
269+
PRIVATE
270+
-g -O0 -fprofile-arcs -ftest-coverage
271+
)
272+
273+
target_link_libraries(
274+
GameAnalytics
275+
PRIVATE
276+
--coverage
277+
)
278+
279+
set(covname cov)
280+
281+
add_custom_target(cov_data
282+
# Cleanup lcov
283+
COMMENT "Resetting code coverage counters to zero."
284+
${LCOV_PATH} --directory . --zerocounters
285+
286+
# Run tests
287+
COMMAND GameAnalyticsUnitTests
288+
289+
# Capturing lcov counters and generating report
290+
COMMAND ${LCOV_PATH} --directory . --capture --output-file ${covname}.info
291+
COMMAND ${LCOV_PATH} --remove ${covname}.info
292+
'${CMAKE_SOURCE_DIR}/source/dependencies/*'
293+
'${CMAKE_SOURCE_DIR}/test/*'
294+
'/usr/*'
295+
'/Applications/Xcode.app/*'
296+
--output-file ${covname}.info.cleaned
297+
)
298+
299+
if (GENHTML_PATH)
300+
add_custom_target(cov
301+
302+
# Cleanup lcov
303+
${LCOV_PATH} --directory . --zerocounters
304+
305+
# Run tests
306+
COMMAND GameAnalyticsUnitTests
307+
308+
# Capturing lcov counters and generating report
309+
COMMAND ${LCOV_PATH} --directory . --capture --output-file ${covname}.info --rc lcov_branch_coverage=1 --rc derive_function_end_line=0
310+
COMMAND ${LCOV_PATH} --remove ${covname}.info
311+
'${CMAKE_SOURCE_DIR}/source/dependencies/*'
312+
'/usr/*'
313+
--output-file ${covname}.info.cleaned
314+
--rc lcov_branch_coverage=1
315+
--rc derive_function_end_line=0
316+
COMMAND ${GENHTML_PATH} -o ${covname} ${covname}.info.cleaned --rc lcov_branch_coverage=1 --rc derive_function_end_line=0
317+
COMMAND ${CMAKE_COMMAND} -E remove ${covname}.info ${covname}.info.cleaned
318+
319+
COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
320+
)
321+
else()
322+
message(WARNING "unable to generate coverage report: missing genhtml")
323+
endif()
324+
325+
else()
326+
message(WARNING "unable to add coverage targets: missing coverage tools")
327+
endif()
328+

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
def run_command(command, shell=True, cwd=None):
88
if os.name == 'nt': # Check if the OS is Windows
99
command = f'powershell.exe -Command "{command}"'
10-
result = subprocess.run(command, shell=shell, check=True, text=True, cwd=cwd)
10+
result = subprocess.run(command, shell=shell, check=True, text=True)
1111
return result
1212

1313
def main():

source/gameanalytics/GAHealth.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ namespace gameanalytics
3535
if((memory > 0) && (_totalMemory > 0))
3636
{
3737
int memoryPercent = std::round(static_cast<double>(memory) / static_cast<double>(_totalMemory) * 100.0);
38-
return memoryPercent;
38+
return std::min(memoryPercent, 100);
3939
}
4040

41-
return -1;
41+
return 0;
42+
4243
}
4344

4445
void GAHealth::doAppMemoryReading(int64_t memory)
@@ -172,4 +173,4 @@ namespace gameanalytics
172173

173174
}
174175
}
175-
}
176+
}

test/GAHealth_test.cpp

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#include <gtest/gtest.h>
2+
#include <gmock/gmock.h>
3+
#include <GAHealth.h>
4+
#include <nlohmann/json.hpp>
5+
6+
using namespace gameanalytics;
7+
using ::testing::Return;
8+
9+
namespace gameanalytics
10+
{
11+
class MockGAPlatform : public GAPlatform
12+
{
13+
public:
14+
MOCK_METHOD(std::string, getOSVersion, (), (override));
15+
MOCK_METHOD(std::string, getDeviceManufacturer, (), (override));
16+
MOCK_METHOD(std::string, getBuildPlatform, (), (override));
17+
MOCK_METHOD(std::string, getPersistentPath, (), (override));
18+
MOCK_METHOD(std::string, getDeviceModel, (), (override));
19+
MOCK_METHOD(std::string, getConnectionType, (), (override));
20+
21+
// Mocking non-pure virtual methods
22+
MOCK_METHOD(std::string, getAdvertisingId, (), (override));
23+
MOCK_METHOD(std::string, getDeviceId, (), (override));
24+
MOCK_METHOD(void, setupUncaughtExceptionHandler, (), (override));
25+
MOCK_METHOD(void, onInit, (), (override));
26+
27+
// Mocking const methods
28+
MOCK_METHOD(std::string, getCpuModel, (), (const, override));
29+
MOCK_METHOD(std::string, getGpuModel, (), (const, override));
30+
MOCK_METHOD(int, getNumCpuCores, (), (const, override));
31+
MOCK_METHOD(int64_t, getTotalDeviceMemory, (), (const, override));
32+
MOCK_METHOD(int64_t, getAppMemoryUsage, (), (const, override));
33+
MOCK_METHOD(int64_t, getSysMemoryUsage, (), (const, override));
34+
MOCK_METHOD(int64_t, getBootTime, (), (const, override));
35+
36+
MockGAPlatform()
37+
{
38+
ON_CALL(*this, getOSVersion).WillByDefault(Return("10.0"));
39+
ON_CALL(*this, getDeviceManufacturer).WillByDefault(Return("GenericManufacturer"));
40+
ON_CALL(*this, getBuildPlatform).WillByDefault(Return("Windows"));
41+
ON_CALL(*this, getPersistentPath).WillByDefault(Return("/persistent/path"));
42+
ON_CALL(*this, getDeviceModel).WillByDefault(Return("DeviceModelX"));
43+
ON_CALL(*this, getConnectionType).WillByDefault(Return("WiFi"));
44+
45+
ON_CALL(*this, getAdvertisingId).WillByDefault(Return("ad-id-123"));
46+
ON_CALL(*this, getDeviceId).WillByDefault(Return("device-id-456"));
47+
ON_CALL(*this, setupUncaughtExceptionHandler).WillByDefault(Return());
48+
ON_CALL(*this, onInit).WillByDefault(Return());
49+
50+
ON_CALL(*this, getCpuModel).WillByDefault(Return("Intel Core i7"));
51+
ON_CALL(*this, getGpuModel).WillByDefault(Return("Nvidia GTX 1080"));
52+
ON_CALL(*this, getNumCpuCores).WillByDefault(Return(8));
53+
ON_CALL(*this, getTotalDeviceMemory).WillByDefault(Return(16384)); // 16GB
54+
ON_CALL(*this, getAppMemoryUsage).WillByDefault(Return(1024)); // 1GB
55+
ON_CALL(*this, getSysMemoryUsage).WillByDefault(Return(2048)); // 2GB
56+
ON_CALL(*this, getBootTime).WillByDefault(Return(30000)); // 30 seconds
57+
}
58+
};
59+
}
60+
61+
62+
// Test subclass to access protected members
63+
class GAHealthTestable : public gameanalytics::GAHealth
64+
{
65+
public:
66+
using gameanalytics::GAHealth::GAHealth; // Inherit constructor
67+
using gameanalytics::GAHealth::_fpsReadings; // Expose protected member for testing
68+
using gameanalytics::GAHealth::_appMemoryUsage; // Expose protected memory usage for testing
69+
using gameanalytics::GAHealth::_sysMemoryUsage; // Expose system memory usage for testing
70+
using gameanalytics::GAHealth::getMemoryPercent;
71+
using gameanalytics::GAHealth::_totalMemory;
72+
};
73+
74+
75+
class GAHealthTest : public ::testing::Test
76+
{
77+
protected:
78+
MockGAPlatform* mockPlatform;
79+
GAHealthTestable* gaHealth;
80+
81+
virtual void SetUp() override
82+
{
83+
mockPlatform = new MockGAPlatform();
84+
gaHealth = new GAHealthTestable(mockPlatform);
85+
}
86+
87+
virtual void TearDown() override
88+
{
89+
delete gaHealth;
90+
delete mockPlatform;
91+
}
92+
};
93+
94+
TEST_F(GAHealthTest, ConstructorInitializesPlatform)
95+
{
96+
EXPECT_CALL(*mockPlatform, getCpuModel()).WillOnce(Return("Intel"));
97+
EXPECT_CALL(*mockPlatform, getNumCpuCores()).WillOnce(Return(4));
98+
EXPECT_CALL(*mockPlatform, getDeviceModel()).WillOnce(Return("Device123"));
99+
EXPECT_CALL(*mockPlatform, getGpuModel()).WillOnce(Return("Nvidia"));
100+
EXPECT_CALL(*mockPlatform, getTotalDeviceMemory()).WillOnce(Return(8192));
101+
102+
gameanalytics::GAHealth health(mockPlatform);
103+
health.enableHardwareTracking = true;
104+
105+
json out;
106+
health.addHealthAnnotations(out);
107+
108+
std::cout << std::setw(4) << out << '\n';
109+
110+
EXPECT_EQ(out["cpu_model"], "Intel");
111+
EXPECT_EQ(out["cpu_num_cores"], 4);
112+
EXPECT_EQ(out["hardware"], "Device123");
113+
}
114+
115+
TEST_F(GAHealthTest, AddHealthAnnotationsIncludesHardwareTracking)
116+
{
117+
EXPECT_CALL(*mockPlatform, getCpuModel()).WillOnce(Return("Intel"));
118+
EXPECT_CALL(*mockPlatform, getNumCpuCores()).WillOnce(Return(4));
119+
EXPECT_CALL(*mockPlatform, getDeviceModel()).WillOnce(Return("Device123"));
120+
121+
GAHealthTestable* _localHealthTracker = new GAHealthTestable(mockPlatform);
122+
123+
_localHealthTracker->enableHardwareTracking = true;
124+
125+
json healthEvent;
126+
_localHealthTracker->addHealthAnnotations(healthEvent);
127+
128+
std::cout << std::setw(4) << healthEvent["cpu_model"] << '\n';
129+
130+
EXPECT_EQ(healthEvent["cpu_model"], "Intel");
131+
EXPECT_EQ(healthEvent["cpu_num_cores"], 4);
132+
EXPECT_EQ(healthEvent["hardware"], "Device123");
133+
}
134+
135+
TEST_F(GAHealthTest, DoFpsReadingIncrementsBucketCorrectly)
136+
{
137+
float testFps = 60.0f;
138+
139+
gaHealth->doFpsReading(testFps);
140+
141+
EXPECT_EQ(gaHealth->_fpsReadings[60], 1);
142+
}
143+
144+
TEST_F(GAHealthTest, GetMemoryPercentReturnsCorrectValue)
145+
{
146+
int percent = gaHealth->getMemoryPercent(4096);
147+
EXPECT_EQ(percent, 25); // 25% memory usage
148+
}
149+
150+
// Test getMemoryPercent with various inputs
151+
TEST_F(GAHealthTest, GetMemoryPercentReturnsCorrectValues)
152+
{
153+
// Case 1: 50% memory usage
154+
int64_t totalMemory = 1000;
155+
gaHealth->_totalMemory = totalMemory;
156+
int memory = 500;
157+
int expectedPercent = 50;
158+
EXPECT_EQ(gaHealth->getMemoryPercent(memory), expectedPercent);
159+
160+
// Case 2: 100% memory usage
161+
memory = 1000;
162+
expectedPercent = 100;
163+
EXPECT_EQ(gaHealth->getMemoryPercent(memory), expectedPercent);
164+
165+
// Case 3: 0% memory usage
166+
memory = 1;
167+
expectedPercent = 0;
168+
EXPECT_EQ(gaHealth->getMemoryPercent(memory), expectedPercent);
169+
170+
// Case 4: More than 100% memory usage (should not happen, but edge case)
171+
memory = 2000;
172+
expectedPercent = 100; // Assuming 100% cap
173+
EXPECT_EQ(gaHealth->getMemoryPercent(memory), expectedPercent);
174+
175+
// Case 5: Negative memory value (should return 0 or handle gracefully)
176+
memory = -100;
177+
expectedPercent = 0; // Assuming a negative value will result in 0%
178+
EXPECT_EQ(gaHealth->getMemoryPercent(memory), expectedPercent);
179+
}
180+
181+
TEST_F(GAHealthTest, AddPerformanceDataIncludesFPSTracking)
182+
{
183+
gaHealth->enableFPSTracking = true;
184+
185+
// Fill FPS readings with some values
186+
gaHealth->_fpsReadings[60] = 5;
187+
gaHealth->_fpsReadings[30] = 2;
188+
189+
json performanceData;
190+
gaHealth->addPerformanceData(performanceData);
191+
192+
193+
json expectedFpsData;
194+
expectedFpsData["fps_data_table"] = gaHealth->_fpsReadings;
195+
196+
EXPECT_EQ(performanceData["fps_data_table"], expectedFpsData["fps_data_table"]);
197+
}
198+
199+
TEST_F(GAHealthTest, AddSDKInitDataIncludesBootTime)
200+
{
201+
gaHealth->enableAppBootTimeTracking = true;
202+
203+
EXPECT_CALL(*mockPlatform, getBootTime()).WillOnce(Return(5000));
204+
205+
json sdkInitEvent;
206+
gaHealth->addSDKInitData(sdkInitEvent);
207+
208+
EXPECT_EQ(sdkInitEvent["app_boot_time"], 5000);
209+
}

0 commit comments

Comments
 (0)