320 lines
No EOL
12 KiB
TypeScript
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
|