Adds hash change e2e test - #210
Conversation
Tests the complete flow from source S3 bucket through document processing, chunking, and vector embeddings generation to destination S3 bucket Signed-off-by: Puneet Punamiya <ppunamiy@redhat.com>
Signed-off-by: Puneet Punamiya <ppunamiy@redhat.com>
Signed-off-by: Puneet Punamiya <ppunamiy@redhat.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new end-to-end test, TestS3Destination, to verify the processing of unstructured data when using S3 as a destination. It includes setup for AWS resources via Localstack, file upload simulations, and verification of embedding generation and hash-based reconciliation. Feedback focuses on improving test stability by using t.Fatal for setup failures to prevent cascading errors and adding nil pointer checks when dereferencing AWS SDK response fields to avoid panics. Additionally, it is recommended to track the port-forwarding process for proper cleanup to avoid background process leaks.
| if err != nil { | ||
| t.Error(err) | ||
| } |
There was a problem hiding this comment.
Using t.Error here allows the test to continue even if the directory cannot be read. This can lead to misleading test results (e.g., comparing empty slices) or panics in subsequent steps. Use t.Fatal to stop the test immediately on setup failures.
| if err != nil { | |
| t.Error(err) | |
| } | |
| files, err := os.ReadDir(testFilesDirectory) | |
| if err != nil { | |
| t.Fatalf("Failed to read test files directory: %v", err) | |
| } |
| pf := exec.Command("kubectl", "port-forward", "-n", namespace, "services/localstack", "4566:4566") | ||
| pf.Stdout = os.Stdout | ||
| pf.Stderr = os.Stderr | ||
| if err := pf.Start(); err != nil { | ||
| return fmt.Errorf("failed to start port-forward: %w", err) | ||
| } |
There was a problem hiding this comment.
The port-forward process started here is not tracked for cleanup. If the test suite finishes or crashes, this process might continue running in the background. Consider returning the exec.Cmd object or adding it to a global registry so it can be killed in the TestMain cleanup phase, similar to how it is handled in testSetup.
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| queueURL = *createQueueOutput.QueueUrl |
There was a problem hiding this comment.
The QueueUrl field in CreateQueueOutput is a pointer. Dereferencing it directly without a nil check can cause a panic if the AWS call returns an unexpected response. It is safer to verify the pointer is not nil before dereferencing.
| queueURL = *createQueueOutput.QueueUrl | |
| if createQueueOutput.QueueUrl == nil { | |
| t.Fatal("QueueUrl is nil") | |
| } | |
| queueURL = *createQueueOutput.QueueUrl |
| if err := operatorUtils.WaitForResourceReady(v1alpha1.UnstructuredDataProductCondition, "unstructureddataproducts.operator.dataverse.redhat.com", dataProductCRName, testNamespace); err != nil { | ||
| t.Error(err) | ||
| } |
There was a problem hiding this comment.
Failures during the setup phase of a test should use t.Fatal or t.Fatalf to stop execution immediately. Continuing after a setup failure often leads to confusing secondary errors or panics in the assessment phase, making debugging more difficult.
| if err := operatorUtils.WaitForResourceReady(v1alpha1.UnstructuredDataProductCondition, "unstructureddataproducts.operator.dataverse.redhat.com", dataProductCRName, testNamespace); err != nil { | |
| t.Error(err) | |
| } | |
| if err := operatorUtils.WaitForResourceReady(v1alpha1.UnstructuredDataProductCondition, "unstructureddataproducts.operator.dataverse.redhat.com", dataProductCRName, testNamespace); err != nil { | |
| t.Fatalf("Unstructured data product CR failed to become healthy: %v", err) | |
| } |
| // look for embeddings files (should have .json extension and contain embeddings) | ||
| foundCount := 0 | ||
| for _, obj := range output.Contents { | ||
| key := *obj.Key |
| if headBefore.ChecksumSHA256 != nil { | ||
| hashBefore = *headBefore.ChecksumSHA256 | ||
| } | ||
| timestampBefore := *headBefore.LastModified |
There was a problem hiding this comment.
Dereferencing LastModified without a nil check can cause a panic. In AWS SDK v2, these fields are pointers and should be checked before access to ensure test stability.
| timestampBefore := *headBefore.LastModified | |
| if headBefore.LastModified == nil { | |
| t.Fatal("LastModified is nil") | |
| } | |
| timestampBefore := *headBefore.LastModified |
| // ensurePortForward ensures a fresh port-forward connection to localstack | ||
| // Kills any existing port-forward and starts a new one | ||
| func ensurePortForward(namespace string) error { | ||
| // Kill any existing port-forward | ||
| exec.Command("pkill", "-f", "port-forward.*localstack").Run() | ||
| time.Sleep(1 * time.Second) | ||
|
|
||
| // Start new port-forward | ||
| log.Println("Starting fresh port-forward for localstack...") | ||
| pf := exec.Command("kubectl", "port-forward", "-n", namespace, "services/localstack", "4566:4566") | ||
| pf.Stdout = os.Stdout | ||
| pf.Stderr = os.Stderr | ||
| if err := pf.Start(); err != nil { | ||
| return fmt.Errorf("failed to start port-forward: %w", err) | ||
| } | ||
|
|
||
| // Give it time to establish | ||
| time.Sleep(3 * time.Second) | ||
| log.Println("Port-forward established") | ||
| return nil | ||
| } | ||
|
|
There was a problem hiding this comment.
I dont think so it is required because user will make sure that while doing port forwarding it should not have any process running , this is making code unnecessarily complex
| // upload files to source S3 bucket | ||
| for _, file := range files { | ||
| if file.IsDir() { | ||
| t.Errorf("subdirectories are not allowed in the test files directory: %s", testFilesDirectory) | ||
| } | ||
|
|
||
| fileContent, err := os.ReadFile(filepath.Join(testFilesDirectory, file.Name())) | ||
| if err != nil { | ||
| t.Error(err) | ||
| } | ||
|
|
||
| key := fmt.Sprintf("%s/%s", sourcePrefix, file.Name()) | ||
| _, err = s3Client.PutObject(ctx, &s3.PutObjectInput{ | ||
| Bucket: aws.String(sourceBucketName), | ||
| Key: aws.String(key), | ||
| Body: bytes.NewReader(fileContent), | ||
| }) | ||
| if err != nil { | ||
| t.Error(err) | ||
| } | ||
| t.Logf("Uploaded test file: %s", key) | ||
| } |
There was a problem hiding this comment.
so after listing all the files in testFilesDirectory , you are uploading all the files to the source bucket -----> files get processed -----------> uploaded to destinationPrefix in destination bucket , right ??.
Just hold your thoughts!!!!!!!!! and read the next comment
| t.Log("Waiting for reconciliation to complete...") | ||
| err = apimachinerywait.PollUntilContextTimeout(ctx, 2*time.Second, 3*time.Minute, false, func(ctx context.Context) (bool, error) { | ||
| currentCR := &v1alpha1.UnstructuredDataProduct{} | ||
| if err := kubeClient.Resources().Get(ctx, dataProductCRName, testNamespace, currentCR); err != nil { | ||
| t.Logf("Failed to get CR: %v", err) | ||
| return false, nil | ||
| } | ||
|
|
||
| // Check if the CR has been reconciled (observed generation matches current generation) | ||
| if currentCR.Status.LastAppliedGeneration != currentCR.Generation { | ||
| t.Logf("Waiting for reconciliation, generation: %d, observed: %d", | ||
| currentCR.Generation, currentCR.Status.LastAppliedGeneration) | ||
| return false, nil | ||
| } | ||
|
|
||
| // Check if the UnstructuredDataProductReady condition is True | ||
| for _, condition := range currentCR.Status.Conditions { | ||
| if condition.Type == v1alpha1.UnstructuredDataProductCondition { | ||
| if condition.Status == metav1.ConditionTrue { | ||
| t.Log("Reconciliation completed successfully") | ||
| return true, nil | ||
| } | ||
| t.Logf("Waiting for condition to be ready, current status: %s, reason: %s", | ||
| condition.Status, condition.Reason) | ||
| return false, nil | ||
| } | ||
| } | ||
|
|
||
| t.Log("UnstructuredDataProductReady condition not found yet") | ||
| return false, nil | ||
| }) |
There was a problem hiding this comment.
we already have WaitForResourceReady function inside the utils_function.go file which automatically waits until CR status true for 10 min , can't we avoid re-writing same logic again ?
| err = apimachinerywait.PollUntilContextTimeout(ctx, 2*time.Second, 5*time.Minute, false, func(ctx context.Context) (bool, error) { | ||
| currentCR := &v1alpha1.UnstructuredDataProduct{} | ||
| if err := kubeClient.Resources().Get(ctx, dataProductCRName, testNamespace, currentCR); err != nil { | ||
| t.Logf("Failed to get CR: %v", err) | ||
| return false, nil | ||
| } | ||
|
|
||
| // Check if the CR has been reconciled | ||
| if currentCR.Status.LastAppliedGeneration != currentCR.Generation { | ||
| t.Logf("Waiting for reconciliation, generation: %d, observed: %d", | ||
| currentCR.Generation, currentCR.Status.LastAppliedGeneration) | ||
| return false, nil | ||
| } | ||
|
|
||
| // Check if the UnstructuredDataProductReady condition is True | ||
| for _, condition := range currentCR.Status.Conditions { | ||
| if condition.Type == v1alpha1.UnstructuredDataProductCondition { | ||
| if condition.Status == metav1.ConditionTrue { | ||
| t.Log("Reconciliation completed successfully") | ||
| return true, nil | ||
| } | ||
| t.Logf("Waiting for condition to be ready, current status: %s, reason: %s", | ||
| condition.Status, condition.Reason) | ||
| return false, nil | ||
| } | ||
| } | ||
|
|
||
| t.Log("UnstructuredDataProductReady condition not found yet") | ||
| return false, nil | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("Timeout waiting for reconciliation to complete: %v", err) | ||
| } |
There was a problem hiding this comment.
same................logic is getting repeated
| }) | ||
| if err != nil { | ||
| t.Fatalf("Failed to upload new file: %v", err) | ||
| } |
There was a problem hiding this comment.
Contd... listing all the files from the destination getting the details of first file, then reading the testFilesDirectory again and finding the files that hasn't been uploaded yet at line 1139, how is it possible because we have uploaded all the files previously.
|
@PuneetPunamiya , what I think , instead of writing this much complex logic like iterating through the directory , ensuring that there are atleast 2 files, finding the non uploaded file and then uploading....verifying
instead of iterating , uploading , simply create file1 , file2 content variable with []byte data
what are your thoughts on this ? |
| feature.Setup( | ||
| func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { | ||
| // Ensure port-forward is alive before setup | ||
| if err := ensurePortForward(testNamespace); err != nil { |
| } | ||
|
|
||
| // create AWS clients | ||
| err = awsclienthandler.NewSourceS3ClientFromConfig(ctx, &awsclienthandler.AWSConfig{ |
There was a problem hiding this comment.
are not all these created in main_test.go or these should be part of config i required for anything apart from file upload
|
closing this as both tests are merged in single MR #202 |
No description provided.