databuild/databuild/dashboard/transformation-tests.ts
Stuart Axelbrooke 24482e2cc4
Some checks are pending
/ setup (push) Waiting to run
Big compile time correctness commit
2025-07-21 19:22:51 -07:00

320 lines
No EOL
12 KiB
TypeScript

// Phase 3.5: Unit tests for transformation functions
// These tests verify that transformation functions prevent the observed runtime failures
import o from 'ospec';
import {
BuildSummary,
BuildDetailResponse,
PartitionSummary,
JobSummary,
ActivityResponse
} from '../client/typescript_generated/src/index';
// Import types directly since we're now in the same ts_project
import {
DashboardActivity,
DashboardBuild,
DashboardPartition,
DashboardJob,
isDashboardActivity,
isDashboardBuild,
isDashboardPartition,
isDashboardJob
} from './types';
// Mock transformation functions for testing (since they're not exported from services.ts)
function transformBuildSummary(apiResponse: BuildSummary): DashboardBuild {
return {
build_request_id: apiResponse.build_request_id,
status_code: apiResponse.status_code,
status_name: apiResponse.status_name,
requested_partitions: apiResponse.requested_partitions, // Keep as PartitionRef array
total_jobs: apiResponse.total_jobs,
completed_jobs: apiResponse.completed_jobs,
failed_jobs: apiResponse.failed_jobs,
cancelled_jobs: apiResponse.cancelled_jobs,
requested_at: apiResponse.requested_at,
started_at: apiResponse.started_at ?? null,
completed_at: apiResponse.completed_at ?? null,
duration_ms: apiResponse.duration_ms ?? null,
cancelled: apiResponse.cancelled,
};
}
function transformBuildDetail(apiResponse: BuildDetailResponse): DashboardBuild {
return {
build_request_id: apiResponse.build_request_id,
status_code: apiResponse.status_code,
status_name: apiResponse.status_name,
requested_partitions: apiResponse.requested_partitions, // Keep as PartitionRef array
total_jobs: apiResponse.total_jobs,
completed_jobs: apiResponse.completed_jobs,
failed_jobs: apiResponse.failed_jobs,
cancelled_jobs: apiResponse.cancelled_jobs,
requested_at: apiResponse.requested_at,
started_at: apiResponse.started_at ?? null,
completed_at: apiResponse.completed_at ?? null,
duration_ms: apiResponse.duration_ms ?? null,
cancelled: apiResponse.cancelled,
};
}
function transformPartitionSummary(apiResponse: any): DashboardPartition {
return {
partition_ref: apiResponse.partition_ref, // Keep as PartitionRef object
status_code: apiResponse.status_code,
status_name: apiResponse.status_name,
last_updated: apiResponse.last_updated ?? null,
build_requests: apiResponse.build_requests || [],
};
}
function transformJobSummary(apiResponse: JobSummary): DashboardJob {
return {
job_label: apiResponse.job_label,
total_runs: apiResponse.total_runs,
successful_runs: apiResponse.successful_runs,
failed_runs: apiResponse.failed_runs,
cancelled_runs: apiResponse.cancelled_runs,
last_run_timestamp: apiResponse.last_run_timestamp,
last_run_status_code: apiResponse.last_run_status_code,
last_run_status_name: apiResponse.last_run_status_name,
average_partitions_per_run: apiResponse.average_partitions_per_run,
recent_builds: apiResponse.recent_builds || [],
};
}
function transformActivityResponse(apiResponse: ActivityResponse): DashboardActivity {
return {
active_builds_count: apiResponse.active_builds_count,
recent_builds: apiResponse.recent_builds.map(transformBuildSummary),
recent_partitions: apiResponse.recent_partitions.map(transformPartitionSummary),
total_partitions_count: apiResponse.total_partitions_count,
system_status: apiResponse.system_status,
graph_name: apiResponse.graph_name,
};
}
// Test Data Mocks
const mockBuildSummary: BuildSummary = {
build_request_id: 'build-123',
status_code: 4, // BUILD_REQUEST_COMPLETED
status_name: 'COMPLETED',
requested_partitions: [{ str: 'partition-1' }, { str: 'partition-2' }],
total_jobs: 5,
completed_jobs: 5,
failed_jobs: 0,
cancelled_jobs: 0,
requested_at: 1640995200000000000, // 2022-01-01 00:00:00 UTC in nanos
started_at: 1640995260000000000, // 2022-01-01 00:01:00 UTC in nanos
completed_at: 1640995320000000000, // 2022-01-01 00:02:00 UTC in nanos
duration_ms: 60000, // 1 minute
cancelled: false
};
const mockPartitionSummary: any = {
partition_ref: { str: 'test-partition' },
status_code: 4, // PARTITION_AVAILABLE
status_name: 'AVAILABLE',
last_updated: 1640995200000000000,
builds_count: 3,
invalidation_count: 0,
build_requests: ['build-123', 'build-124'],
last_successful_build: 'build-123'
};
const mockJobSummary: JobSummary = {
job_label: '//:test-job',
total_runs: 10,
successful_runs: 9,
failed_runs: 1,
cancelled_runs: 0,
average_partitions_per_run: 2.5,
last_run_timestamp: 1640995200000000000,
last_run_status_code: 3, // JOB_COMPLETED
last_run_status_name: 'COMPLETED',
recent_builds: ['build-123', 'build-124']
};
const mockActivityResponse: ActivityResponse = {
active_builds_count: 2,
recent_builds: [mockBuildSummary],
recent_partitions: [mockPartitionSummary],
total_partitions_count: 100,
system_status: 'healthy',
graph_name: 'test-graph'
};
// Test Suite
o.spec('Transformation Functions', () => {
o('transformBuildSummary handles status fields correctly', () => {
const result = transformBuildSummary(mockBuildSummary);
// The key fix: status_name should be a string, status_code a number
o(typeof result.status_code).equals('number');
o(typeof result.status_name).equals('string');
o(result.status_name).equals('COMPLETED');
// This should not throw (preventing the original runtime error)
o(() => result.status_name.toLowerCase()).notThrows('status_name.toLowerCase should work');
});
o('transformBuildSummary handles null optional fields', () => {
const buildWithNulls: BuildSummary = {
...mockBuildSummary,
started_at: null,
completed_at: null,
duration_ms: null
};
const result = transformBuildSummary(buildWithNulls);
// Explicit null handling prevents undefined property access
o(result.started_at).equals(null);
o(result.completed_at).equals(null);
o(result.duration_ms).equals(null);
});
o('transformPartitionSummary preserves PartitionRef objects correctly', () => {
const result = transformPartitionSummary(mockPartitionSummary);
// The key fix: partition_ref should remain as PartitionRef object
o(typeof result.partition_ref).equals('object');
o(result.partition_ref.str).equals('test-partition');
// This should not throw (preventing original runtime errors)
o(() => result.partition_ref.str.toLowerCase()).notThrows('partition_ref.str.toLowerCase should work');
});
o('transformPartitionSummary handles missing arrays safely', () => {
const partitionWithoutArray: any = {
...mockPartitionSummary
};
delete partitionWithoutArray.build_requests;
const result = transformPartitionSummary(partitionWithoutArray);
// Should default to empty array, preventing length/iteration errors
o(Array.isArray(result.build_requests)).equals(true);
o(result.build_requests.length).equals(0);
});
o('transformJobSummary handles status fields correctly', () => {
const result = transformJobSummary(mockJobSummary);
// The key fix: both status code and name should be preserved
o(typeof result.last_run_status_code).equals('number');
o(typeof result.last_run_status_name).equals('string');
o(result.last_run_status_name).equals('COMPLETED');
// This should not throw
o(() => result.last_run_status_name.toLowerCase()).notThrows('last_run_status_name.toLowerCase should work');
});
o('transformActivityResponse maintains structure consistency', () => {
const result = transformActivityResponse(mockActivityResponse);
// Should pass our type guard
o(isDashboardActivity(result)).equals(true);
// All nested objects should be properly transformed
o(result.recent_builds.length).equals(1);
o(typeof result.recent_builds[0]?.status_name).equals('string');
o(result.recent_partitions.length).equals(1);
o(typeof result.recent_partitions[0]?.partition_ref).equals('object');
o(typeof result.recent_partitions[0]?.partition_ref.str).equals('string');
});
o('transformations prevent original runtime failures', () => {
const result = transformActivityResponse(mockActivityResponse);
// These are the exact patterns that caused runtime failures:
// 1. status_name.toLowerCase() - should not crash
result.recent_builds.forEach((build: DashboardBuild) => {
o(() => build.status_name.toLowerCase()).notThrows('build.status_name.toLowerCase should work');
o(build.status_name.toLowerCase()).equals('completed');
});
// 2. partition_ref.str access - should access string property
result.recent_partitions.forEach((partition: DashboardPartition) => {
o(typeof partition.partition_ref).equals('object');
o(typeof partition.partition_ref.str).equals('string');
o(() => partition.partition_ref.str.toLowerCase()).notThrows('partition.partition_ref.str.toLowerCase should work');
});
// 3. Null/undefined handling - should be explicit
result.recent_builds.forEach((build: DashboardBuild) => {
// These fields can be null but never undefined
o(build.started_at === null || typeof build.started_at === 'number').equals(true);
o(build.completed_at === null || typeof build.completed_at === 'number').equals(true);
o(build.duration_ms === null || typeof build.duration_ms === 'number').equals(true);
});
});
});
// Edge Cases and Error Conditions
o.spec('Transformation Edge Cases', () => {
o('handles empty arrays correctly', () => {
const emptyActivity: ActivityResponse = {
...mockActivityResponse,
recent_builds: [],
recent_partitions: []
};
const result = transformActivityResponse(emptyActivity);
o(Array.isArray(result.recent_builds)).equals(true);
o(result.recent_builds.length).equals(0);
o(Array.isArray(result.recent_partitions)).equals(true);
o(result.recent_partitions.length).equals(0);
});
o('handles malformed PartitionRef gracefully', () => {
const malformedPartition: any = {
...mockPartitionSummary,
partition_ref: { str: '' } // Empty string
};
const result = transformPartitionSummary(malformedPartition);
o(typeof result.partition_ref.str).equals('string');
o(result.partition_ref.str).equals('');
});
o('transformations produce valid dashboard types', () => {
// Test that all transformation results pass type guards
const transformedBuild = transformBuildSummary(mockBuildSummary);
const transformedPartition = transformPartitionSummary(mockPartitionSummary);
const transformedJob = transformJobSummary(mockJobSummary);
const transformedActivity = transformActivityResponse(mockActivityResponse);
o(isDashboardBuild(transformedBuild)).equals(true);
o(isDashboardPartition(transformedPartition)).equals(true);
o(isDashboardJob(transformedJob)).equals(true);
o(isDashboardActivity(transformedActivity)).equals(true);
});
});
// Performance and Memory Tests
o.spec('Transformation Performance', () => {
o('transforms large datasets efficiently', () => {
const largeActivity: ActivityResponse = {
...mockActivityResponse,
recent_builds: Array(1000).fill(mockBuildSummary),
recent_partitions: Array(1000).fill(mockPartitionSummary)
};
const start = Date.now();
const result = transformActivityResponse(largeActivity);
const duration = Date.now() - start;
// Should complete transformation in reasonable time
o(duration < 1000).equals(true); // Less than 1 second
o(result.recent_builds.length).equals(1000);
o(result.recent_partitions.length).equals(1000);
});
});
// Export default removed - tests are run by importing this file