Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Begin integrating ixfr package and stupidns package #107

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

zluudg
Copy link
Collaborator

@zluudg zluudg commented Jan 28, 2025

Began integrating ixfr code from another project
Also integrating small test framework used in same project

Not used in tdns yet, but can be imported and used by other projects

Summary by CodeRabbit

Here are the release notes for this pull request:

New Features

  • Introduced StupiDNS, a lightweight DNS server framework for testing
  • Added Incremental Zone Transfer (IXFR) functionality for DNS record management
  • Implemented comprehensive test infrastructure with integration and sanity tests

Improvements

  • Created modular testing utilities for DNS record comparison
  • Added support for ordered and unordered slice comparisons
  • Enhanced test case configuration through YAML-based test definitions

Testing

  • Developed integration test suite for DNS server functionality
  • Added test cases for simple DNS requests and IXFR scenarios
  • Implemented flexible test result validation mechanisms

Infrastructure

  • Set up Go modules for stupidns and IXFR packages
  • Added Makefiles for running tests across different packages

Copy link
Contributor

coderabbitai bot commented Jan 28, 2025

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 golangci-lint (1.62.2)

level=error msg="Running error: context loading failed: no go files to analyze: running go mod tidy may solve the problem"

Walkthrough

The pull request introduces a comprehensive testing framework for a DNS-related project, focusing on creating a lightweight DNS server called StupiDNS and implementing Incremental Zone Transfer (IXFR) functionality. The changes span multiple packages, including stupidns, ixfr, and integration tests, providing robust mechanisms for testing DNS server interactions, record comparisons, and transfer scenarios.

Changes

File Change Summary
stupidns/common.go Added core testing utilities, constants, structs for test cases, and dispatch tables for result fetching and checking
stupidns/result_checkers.go Implemented functions for comparing string slices with various comparison strategies
stupidns/result_fetchers.go Added utility functions for generating test result slices
stupidns/stupidns.go Created a simple DNS server implementation with queuing and response handling
tdns/ixfr/diffsequence.go Introduced DiffSequence for managing DNS record differences
tdns/ixfr/ixfr.go Implemented IXFR response parsing and management
tests/ Added integration test suites for sanity checks, simple requests, and IXFR scenarios

Sequence Diagram

sequenceDiagram
    participant Client
    participant StupiDNS
    participant TestFramework

    TestFramework->>StupiDNS: Create Server
    TestFramework->>StupiDNS: Queue Responses
    TestFramework->>StupiDNS: Start Server
    Client->>StupiDNS: Send DNS Query
    StupiDNS-->>Client: Return Queued Response
    TestFramework->>TestFramework: Validate Response
Loading

Poem

🐰 Hoppity hop, a DNS delight!
Testing servers with all my might
StupiDNS dances, records take flight
Integration tests shine so bright
Code hops along, everything's right! 🌈

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (17)
tdns/ixfr/ixfr.go (2)

1-2: Ensure consistent naming conventions.
You're using ixfr for both the package name and the struct name. Although not strictly an error, consider updating one or the other to make references clearer within the code and documentation.


124-138: Revisit GetCompressed naming.
The method merges multiple DiffSequence objects into one aggregated list, returning a new sequence with combined AddedRecords and DeletedRecords. The name is valid but consider something more descriptive like MergeDiffSequences or FlattenDiffs.

stupidns/result_fetchers.go (1)

4-43: Consider refactoring repetitive slice initialization logic.

The test utility functions follow a similar pattern of slice initialization. Consider introducing a helper function to reduce code duplication.

+func makeStringSlice(values ...string) []string {
+    ret := make([]string, len(values))
+    copy(ret, values)
+    return ret
+}

 func getFoo(obj ...interface{}) []string {
-    ret := make([]string, 1)
-    ret[0] = "foo"
-    return ret
+    return makeStringSlice("foo")
 }

 func getBarFoo(obj ...interface{}) []string {
-    ret := make([]string, 2)
-    ret[0] = "bar"
-    ret[1] = "foo"
-    return ret
+    return makeStringSlice("bar", "foo")
 }
tdns/ixfr/diffsequence.go (2)

78-131: Improve error handling and simplify slice operations.

A few suggestions for improvement:

  1. The slice operation slice[1:len(slice)] can be simplified to slice[1:]
  2. Error messages could be more descriptive by including the actual error and record details
  3. Consider pre-allocating the results slice with an estimated capacity
-       diff[key] = slice[1:len(slice)]
+       diff[key] = slice[1:]
-               panic("Error calculating diff between RR slices")
+               panic(fmt.Sprintf("Error calculating diff between RR slices: %v for record %s", err, s))
-       rrs := make([]dns.RR, 0)
+       rrs := make([]dns.RR, 0, len(diff))
🧰 Tools
🪛 golangci-lint (1.62.2)

109-109: S1010: should omit second index in slice, s[a:len(s)] is identical to s[a:]

(gosimple)


49-71: Enhance error handling in Add methods.

The error handling in both AddAdded and AddDeleted could be more informative by including the failing record string in the panic message.

-               panic("Error adding RR to 'added' slice")
+               panic(fmt.Sprintf("Error adding RR to 'added' slice: %v for record %s", err, rrStr))
-               panic("Error adding RR to 'deleted' slice")
+               panic(fmt.Sprintf("Error adding RR to 'deleted' slice: %v for record %s", err, rrStr))
tdns/ixfr/diffsequence_test.go (2)

7-21: Consider using table-driven tests for better test organization.

The TestDiffSequenceEquals test could be refactored into a table-driven test to make it easier to add more test cases.

 func TestDiffSequenceEquals(t *testing.T) {
-       seq1 := CreateDiffSequence(2, 3)
-       seq1.AddAdded("nezu.jain.ad.jp A 133.69.136.5")
-       seq1.AddDeleted("jain-bb.jain.ad.jp A 133.69.136.4")
-       seq1.AddDeleted("jain-bb.jain.ad.jp A 192.41.197.2")
-
-       seq2 := CreateDiffSequence(2, 3)
-       seq2.AddDeleted("jain-bb.jain.ad.jp A 192.41.197.2")
-       seq2.AddDeleted("jain-bb.jain.ad.jp A 133.69.136.4")
-       seq2.AddAdded("nezu.jain.ad.jp A 133.69.136.5")
-
-       if !seq1.Equals(seq2) {
-               t.Errorf("Sequences not equal!")
-       }
+       tests := []struct {
+               name     string
+               seq1     DiffSequence
+               seq2     DiffSequence
+               wantEqual bool
+       }{
+               {
+                       name: "same records different order",
+                       seq1: func() DiffSequence {
+                               seq := CreateDiffSequence(2, 3)
+                               seq.AddAdded("nezu.jain.ad.jp A 133.69.136.5")
+                               seq.AddDeleted("jain-bb.jain.ad.jp A 133.69.136.4")
+                               seq.AddDeleted("jain-bb.jain.ad.jp A 192.41.197.2")
+                               return seq
+                       }(),
+                       seq2: func() DiffSequence {
+                               seq := CreateDiffSequence(2, 3)
+                               seq.AddDeleted("jain-bb.jain.ad.jp A 192.41.197.2")
+                               seq.AddDeleted("jain-bb.jain.ad.jp A 133.69.136.4")
+                               seq.AddAdded("nezu.jain.ad.jp A 133.69.136.5")
+                               return seq
+                       }(),
+                       wantEqual: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := tt.seq1.Equals(tt.seq2); got != tt.wantEqual {
+                               t.Errorf("DiffSequence.Equals() = %v, want %v", got, tt.wantEqual)
+                       }
+               })
+       }

36-38: Improve test error messages.

The error message could be more descriptive by including the actual and expected values.

-               t.Errorf("Got: %+v\n Want: %+v\n", got, want)
+               t.Errorf("GetAdded() got = %+v, want %+v", got, want)
stupidns/common.go (1)

68-77: Consider using error wrapping instead of panic.

The Setup and Teardown functions use panic for error handling. Consider returning errors instead to allow better error handling by callers.

-func Setup() {
+func Setup() error {
        var err error
        TEST_RUNDIR, err = os.MkdirTemp("", "tdns_integration_test_*")
        TEST_RUNDIR += "/"

        if err != nil {
-               panic("Error setting up integration tests")
+               return fmt.Errorf("error setting up integration tests: %w", err)
        }
+       return nil
 }

-func Teardown() {
+func Teardown() error {
        err := os.RemoveAll(TEST_RUNDIR)

        if err != nil {
-               panic("Error tearing down integration tests")
+               return fmt.Errorf("error tearing down integration tests: %w", err)
        }
+       return nil
 }

Also applies to: 79-86

stupidns/result_checkers.go (1)

28-58: Consider pre-allocating the map with the correct size.

The unorderedSliceCompare function could be optimized by pre-allocating the map with the correct size.

-       diff := make(map[string]int, len(got))
+       // Pre-allocate with size of non-empty strings
+       size := 0
+       for _, g := range got {
+               if g != "" {
+                       size++
+               }
+       }
+       diff := make(map[string]int, size)
tdns/ixfr/ixfr_test.go (2)

9-47: Consider using table-driven tests for better maintainability.

The test data could be structured as a table of test cases, making it easier to add more test scenarios.

Here's an example refactor:

+type ixfrTestCase struct {
+	name     string
+	response []string
+	want     Ixfr
+}
+
 func TestIxfr(t *testing.T) {
-	/* Should be equivalent to the example in RFC 1995 */
-	response := new(dns.Msg)
-	response.Answer = makeRRSlice(
-		"jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
+	tests := []ixfrTestCase{
+		{
+			name: "RFC 1995 example",
+			response: []string{
+				"jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
+				// ... other records
+			},
+			want: func() Ixfr {
+				wanted := Ixfr{
+					InitialSOASerial: 1,
+					FinalSOASerial:   3,
+					DiffSequences:    []DiffSequence{},
+				}
+				// ... setup wanted state
+				return wanted
+			}(),
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			response := new(dns.Msg)
+			response.Answer = makeRRSlice(tc.response...)
+			got := IxfrFromResponse(response)
+			if !got.Equals(tc.want) {
+				t.Errorf("Got:\n %+v\n Want:\n %+v", got, tc.want)
+			}
+		})
+	}

49-78: Reduce code duplication in test data setup.

TestGetAdded and TestGetDeleted use the same test data setup. Consider extracting the common setup into a helper function.

Example refactor:

+func setupTestResponse() *dns.Msg {
+	response := new(dns.Msg)
+	response.Answer = makeRRSlice(
+		"jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
+		// ... other records
+	)
+	return response
+}
+
 func TestGetAdded(t *testing.T) {
-	/* Should be equivalent to the example in RFC 1995 */
-	response := new(dns.Msg)
-	response.Answer = makeRRSlice(
+	response := setupTestResponse()

Also applies to: 80-106

tests/Makefile (1)

1-3: LGTM! Consider adding test parallelization.

The integration test target is well-configured with appropriate flags. Consider adding the -parallel flag to control test parallelization, especially since DNS tests might have port conflicts when run in parallel.

 .PHONY: integration
 integration:
-	go test -count=1 -tags integration -run TestIntegration* -v ./...
+	go test -count=1 -parallel 1 -tags integration -run TestIntegration* -v ./...
tests/testdata/sanity_basic_ordered.yaml (1)

1-11: Add test documentation and ensure consistent port allocation.

The test configuration needs documentation and port allocation strategy:

  1. Add descriptive comments about the ordered comparison test case
  2. Consider using a port allocation strategy across test files
+# Test case: Ordered Comparison
+# Purpose: Validates that getFooBar fetcher returns results in correct order
+# Expected: Returns ["foo", "bar"] in exact order
 mqueue:
   - [] # Server not used in this test
 expected:
   - "foo"
   - "bar"
 fetcher: "getFooBar"
 checker: "orderedSliceCompare"
 runs: 1

Consider implementing a central port allocation mechanism in the test framework to prevent conflicts and make port management more maintainable.

tests/testdata/sanity_basic_unordered.yaml (1)

1-11: Document test case and consider test organization.

The test configuration needs:

  1. Documentation explaining the unordered comparison test case
  2. Consider organizing related test cases in a structured way
+# Test case: Unordered Comparison
+# Purpose: Validates that order-independent comparison works correctly
+# Expected: Returns ["foo", "bar"] in any order using getBarFoo fetcher
 mqueue:
   - [] # Server not used in this test
 expected:
   - "foo"
   - "bar"
 fetcher: "getBarFoo"
 checker: "unorderedSliceCompare"
 runs: 1

Consider organizing related test cases (basic, ordered, unordered) in a structured way:

  1. Group related test files in subdirectories
  2. Use a common configuration for shared settings
  3. Implement test case dependencies if needed
tests/testdata/ixfr_integration_two_records.yaml (2)

4-5: Consider using example.test instead of example.com

The test currently uses example.com which is a reserved domain. Consider using example.test which is specifically reserved for testing purposes according to RFC 6761.

-- - "example.com. 190 IN A 23.192.228.84"
-  - "example.com. 300 IN A 96.7.128.175"
++ - "example.test. 190 IN A 23.192.228.84"
+  - "example.test. 300 IN A 96.7.128.175"

13-14: Document port significance

The specific port number (53530) might have significance for IXFR integration tests. Consider adding a comment explaining why this port was chosen.

tests/testdata/simple_request_two_records.yaml (1)

1-3: Enhance test documentation

The comment should clarify how this "simple request" test differs from the IXFR integration test, as they currently appear to test the same functionality.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb5423c and c593f09.

⛔ Files ignored due to path filters (3)
  • stupidns/go.sum is excluded by !**/*.sum
  • tdns/ixfr/go.sum is excluded by !**/*.sum
  • tests/go.sum is excluded by !**/*.sum
📒 Files selected for processing (26)
  • stupidns/common.go (1 hunks)
  • stupidns/go.mod (1 hunks)
  • stupidns/result_checkers.go (1 hunks)
  • stupidns/result_fetchers.go (1 hunks)
  • stupidns/stupidns.go (1 hunks)
  • tdns/ixfr/Makefile (1 hunks)
  • tdns/ixfr/diffsequence.go (1 hunks)
  • tdns/ixfr/diffsequence_test.go (1 hunks)
  • tdns/ixfr/go.mod (1 hunks)
  • tdns/ixfr/ixfr.go (1 hunks)
  • tdns/ixfr/ixfr_test.go (1 hunks)
  • tdns/ixfr/util.go (1 hunks)
  • tdns/ixfr/util_test.go (1 hunks)
  • tests/Makefile (1 hunks)
  • tests/README.md (1 hunks)
  • tests/go.mod (1 hunks)
  • tests/ixfr_integration_test.go (1 hunks)
  • tests/main_test.go (1 hunks)
  • tests/sanity_test.go (1 hunks)
  • tests/simple_test.go (1 hunks)
  • tests/testdata/ixfr_integration_two_records.yaml (1 hunks)
  • tests/testdata/sanity_basic.yaml (1 hunks)
  • tests/testdata/sanity_basic_ordered.yaml (1 hunks)
  • tests/testdata/sanity_basic_unordered.yaml (1 hunks)
  • tests/testdata/simple_request.yaml (1 hunks)
  • tests/testdata/simple_request_two_records.yaml (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • tests/README.md
  • tests/go.mod
  • stupidns/go.mod
  • tdns/ixfr/go.mod
🧰 Additional context used
🪛 golangci-lint (1.62.2)
tdns/ixfr/diffsequence.go

109-109: S1010: should omit second index in slice, s[a:len(s)] is identical to s[a:]

(gosimple)

stupidns/common.go

26-26: structtag: struct field tag yaml:fetcher not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)


27-27: structtag: struct field tag yaml:checker not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)


28-28: structtag: struct field tag yaml:runs not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)


29-29: structtag: struct field tag yaml:config not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)

stupidns/stupidns.go

82-82: Error return value of s.srv.Shutdown is not checked

(errcheck)


45-45: copylocks: return copies lock value: github.com/zluudg/tdns/stupidns.StupiDNS contains github.com/miekg/dns.Server contains sync.RWMutex

(govet)

tdns/ixfr/ixfr.go

9-9: var logger is unused

(unused)

🔇 Additional comments (23)
tdns/ixfr/ixfr.go (8)

3-7: Consider selective imports.
The import block is straightforward, but you might want to import only what’s necessary to reduce potential import bloat. If you plan to expand usage of fmt or rely on other sub-packages from github.com/miekg/dns, keep as is.


11-17: Struct definition looks good.
The Ixfr struct captures the key fields for incremental transfers, including InitialSOASerial, FinalSOASerial, and the diff sequences. This is clear and comprehensible.


19-21: Revisit public API usage of SetLoggerHandle.
Great approach to allow external injection of a logging function. But currently, it's not used anywhere else in this file. If you plan to log in IxfrFromResponse or other methods, integrate it, or remove the logger entirely.


23-25: Straightforward method.
AddDiffSequence is concise and clean.


27-47: Ensure Equals handles all fields consistently.
The equality check is thorough for InitialSOASerial, FinalSOASerial, and each DiffSequence. Make sure to also confirm if you want to compare IsAxfr and AxfrRRs for equality in the future.


140-143: Good aggregator for added records.
GetAdded simply defers to GetCompressed() and returns the “added” portion. Straightforward and consistent.


145-148: Consistent aggregator for deleted records.
Similar to GetAdded, GetDeleted is a pass-through to aggregated data. This is consistent.


49-122: 🛠️ Refactor suggestion

Improve safety and error handling in IxfrFromResponse.

  1. Potential index out-of-range: You directly access rsp.Answer[1] at line 55 and rsp.Answer[0] at line 63. If rsp.Answer is shorter than expected, it will panic.
  2. Use of panic: Consider returning an error rather than calling panic in production/library code. Panics can crash the entire program, whereas returning an error allows consumers to handle the case gracefully.
  3. Toggle logic: The _isAdding_ toggle between added and deleted records is well-structured. Ensure test coverage for boundary conditions when switching states.
  4. Line 88: The block comment references an exception for “the first SOA in the diff sequence list,” which is a subtle edge case. Ensure you have a test that covers the scenario where the first record and the last record are SOAs.
tests/main_test.go (3)

1-3: Build constraint verified.
Specifying //go:build integration ensures this file is only included in the integration build. This is a helpful organizational approach for test environments.


4-9: Package and imports look fine.
The tests package dedicated to integration tests is easy to track. Dependence on os and testing is minimal and appropriate.


11-14: Use TestMain for test setup/teardown.
This function cleanly delegates test execution to m.Run(). If you need heavier initialization or cleanup steps in the future, this is the right place for it.

tdns/ixfr/util.go (3)

1-2: Package boundary noted.
Keeping all IXFR helper functions within the same ixfr package is sensible.


3-5: Minimal imports are appropriate.
github.com/miekg/dns is the key dependency here, so no concerns.


21-49: Pragmatic record comparison in rrEquals.

  1. Using a map to count occurrences is an efficient and clean approach to test equality with duplicates.
  2. Consider consolidating nil checks or proactively filtering nil items to keep the logic simpler.
tests/sanity_test.go (1)

69-71: ⚠️ Potential issue

Empty test run loop requires implementation.

The test run loop is empty and needs to be implemented.

tdns/ixfr/diffsequence.go (2)

9-23: LGTM! Clean struct design with proper initialization.

The DiffSequence struct and its constructor are well-designed, with clear field names and proper initialization of empty slices.


73-77: Consider documenting the current behavior for differing record counts.

The TODO comment raises an important question about handling different numbers of added/deleted records. Until this is implemented, it's crucial to document the current behavior to prevent confusion.

stupidns/result_checkers.go (1)

11-23: Optimize slice comparison by early length check.

The orderedSliceCompare function could be more efficient by checking lengths before iterating.

The implementation is correct and well-documented.

tests/ixfr_integration_test.go (1)

41-44: Apply the same fixes as in simple_test.go.

This file has the same issues as simple_test.go:

  1. YAML unmarshaling error not properly handled
  2. Connection retry mechanism needs improvement
  3. Response validation using panic instead of t.Fatal

Please apply the same fixes as suggested in the simple_test.go review.

Also applies to: 77-91, 101-106

tdns/ixfr/Makefile (1)

1-3: LGTM!

The Makefile correctly defines the test target and marks it as .PHONY.

tests/testdata/sanity_basic.yaml (1)

1-10: Add test case documentation and port validation.

While the test configuration is valid, consider:

  1. Adding a comment block explaining the test's purpose and expected behavior
  2. Implementing port availability checks before test execution
+# Test case: Basic Functionality
+# Purpose: Validates the basic fetcher and checker functionality
+# Expected: Returns "foo" using getFoo fetcher
 mqueue:
   - [] # Server not used in this test
 expected:
   - "foo"
 fetcher: "getFoo"
 checker: "orderedSliceCompare"
 runs: 1

Let's verify port availability across all test files:

✅ Verification successful

Add test case documentation to improve maintainability

The port allocation is well-managed with unique sequential ports (53530-53535) across test files. Consider adding the suggested documentation to improve test maintainability:

+# Test case: Basic Functionality
+# Purpose: Validates the basic fetcher and checker functionality
+# Expected: Returns "foo" using getFoo fetcher
 mqueue:
   - [] # Server not used in this test
 expected:
   - "foo"
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for port conflicts in test configurations
echo "Checking for port conflicts in test configurations..."
rg -l "port: \"[0-9]+\"" tests/testdata/
for file in $(rg -l "port: \"[0-9]+\"" tests/testdata/); do
  echo "Ports in $file:"
  rg "port: \"([0-9]+)\"" "$file" -o -r '$1'
done

Length of output: 1959

tests/testdata/simple_request.yaml (1)

1-12: Consider DNS-specific validations and edge cases.

While the test case is well-documented, consider:

  1. Adding validation for DNS record format
  2. Including edge cases (e.g., CNAME resolution, multiple A records)
  3. Testing TTL boundary conditions

Add additional test cases:

# Test case: Multiple A Records
mqueue:
  - - "example.com. 190 IN A 23.192.228.84"
    - "example.com. 190 IN A 23.192.228.85"
expected:
  - "23.192.228.84"
  - "23.192.228.85"
fetcher: "getFromBucket"
checker: "orderedSliceCompare"
runs: 1

# Test case: CNAME Resolution
mqueue:
  - - "example.com. 190 IN CNAME cdn.example.com."
    - "cdn.example.com. 190 IN A 23.192.228.84"
expected:
  - "23.192.228.84"
fetcher: "getFromBucket"
checker: "orderedSliceCompare"
runs: 1

Let's verify the DNS record format:

tests/testdata/ixfr_integration_two_records.yaml (1)

6-8: Consider validating TTL values

The test only validates IP addresses but ignores TTL values. Consider adding TTL validation if these values are significant for IXFR functionality.

Would you like me to propose an extended test case that includes TTL validation?

"github.com/miekg/dns"
)

var logger = fmt.Printf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove or use the logger variable.
The static analysis tool flags logger as unused. If you don't plan to use it, remove it to reduce clutter.

-var logger = fmt.Printf
+// var logger = fmt.Printf
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var logger = fmt.Printf
// var logger = fmt.Printf
🧰 Tools
🪛 golangci-lint (1.62.2)

9-9: var logger is unused

(unused)

Comment on lines +7 to +19
func makeRRSlice(rrs ...string) []dns.RR {
rrSlice := make([]dns.RR, len(rrs))

for i, r := range rrs {
rr, err := dns.NewRR(r)
if err != nil {
panic("Oh no, could not create list!")
}
rrSlice[i] = rr
}

return rrSlice
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid panic in makeRRSlice.
While convenient in a test or prototype scenario, in production code consider returning an error instead of panicking. This will give calling functions more control over error handling.

-func makeRRSlice(rrs ...string) []dns.RR {
+func makeRRSlice(rrs ...string) ([]dns.RR, error) {
    rrSlice := make([]dns.RR, len(rrs))
    ...
    if err != nil {
-       panic("Oh no, could not create list!")
+       return nil, fmt.Errorf("could not create RR: %v", err)
    }
    ...
-   return rrSlice
+   return rrSlice, nil
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func makeRRSlice(rrs ...string) []dns.RR {
rrSlice := make([]dns.RR, len(rrs))
for i, r := range rrs {
rr, err := dns.NewRR(r)
if err != nil {
panic("Oh no, could not create list!")
}
rrSlice[i] = rr
}
return rrSlice
}
func makeRRSlice(rrs ...string) ([]dns.RR, error) {
rrSlice := make([]dns.RR, len(rrs))
for i, r := range rrs {
rr, err := dns.NewRR(r)
if err != nil {
return nil, fmt.Errorf("could not create RR: %v", err)
}
rrSlice[i] = rr
}
return rrSlice, nil
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Let's try to avoid panic() and log.Fatalf() in the library itself.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johanix Thank you for confirming. I'll make sure to flag any usage of panic() or log.Fatalf() in library code in future reviews.

(`^´)=͟͟͞͞🥕


✏️ Learnings added
Learnt from: johanix
PR: johanix/tdns#107
File: tdns/ixfr/util.go:7-19
Timestamp: 2025-02-03T16:12:44.452Z
Learning: In the tdns library code, avoid using panic() or log.Fatalf(). Instead, functions should return errors to allow proper error handling by the calling code.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +45 to +52
func getFromBucket(obj ...interface{}) []string {
if len(obj) != 1 {
panic("\"getFromBucket\" requires one argument!")
}

var b Bucket = obj[0].(Bucket)
return b.Get()
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add type assertion error handling in getFromBucket.

The type assertion obj[0].(Bucket) will panic if the assertion fails. Consider adding proper error handling.

 func getFromBucket(obj ...interface{}) []string {
     if len(obj) != 1 {
         panic("\"getFromBucket\" requires one argument!")
     }
 
-    var b Bucket = obj[0].(Bucket)
+    b, ok := obj[0].(Bucket)
+    if !ok {
+        panic(fmt.Sprintf("Expected Bucket type, got %T", obj[0]))
+    }
     return b.Get()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func getFromBucket(obj ...interface{}) []string {
if len(obj) != 1 {
panic("\"getFromBucket\" requires one argument!")
}
var b Bucket = obj[0].(Bucket)
return b.Get()
}
func getFromBucket(obj ...interface{}) []string {
if len(obj) != 1 {
panic("\"getFromBucket\" requires one argument!")
}
b, ok := obj[0].(Bucket)
if !ok {
panic(fmt.Sprintf("Expected Bucket type, got %T", obj[0]))
}
return b.Get()
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this one too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johanix Thanks for confirming! I'll make sure to remember this pattern for future reviews.


✏️ Learnings added
Learnt from: johanix
PR: johanix/tdns#107
File: stupidns/result_fetchers.go:45-52
Timestamp: 2025-02-03T16:13:42.215Z
Learning: In Go code, type assertions should include error handling with descriptive panic messages, using the pattern: value, ok := x.(Type); if !ok { panic(fmt.Sprintf("Expected Type, got %T", x)) }

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines +27 to +43
func TestRrEqualsWhitespaceDiff(t *testing.T) {
want := make([]dns.RR, 0)
rr, _ := dns.NewRR("jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
want = append(want, rr)
rr, _ = dns.NewRR("jain-bb.jain.ad.jp A 133.69.136.3")
want = append(want, rr)

otherWant := make([]dns.RR, 0)
rr, _ = dns.NewRR("jain-bb.jain.ad.jp A 133.69.136.3")
otherWant = append(otherWant, rr)
rr, _ = dns.NewRR("jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
otherWant = append(otherWant, rr)

if !rrEquals(want, otherWant) {
t.Errorf("dns.RR Equality test failed!")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Apply similar improvements to TestRrEqualsWhitespaceDiff.

The same issues with error handling and test structure exist in this test function.

Consider applying the same table-driven test pattern and error handling improvements as suggested for TestRrEquals.

Comment on lines +9 to +25
func TestRrEquals(t *testing.T) {
want := make([]dns.RR, 0)
rr, _ := dns.NewRR("jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
want = append(want, rr)
rr, _ = dns.NewRR("jain-bb.jain.ad.jp A 133.69.136.3")
want = append(want, rr)

otherWant := make([]dns.RR, 0)
rr, _ = dns.NewRR("jain-bb.jain.ad.jp A 133.69.136.3")
otherWant = append(otherWant, rr)
rr, _ = dns.NewRR("jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
otherWant = append(otherWant, rr)

if !rrEquals(want, otherWant) {
t.Errorf("dns.RR Equality test failed!")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and test structure in TestRrEquals.

The test function ignores potential errors from dns.NewRR and uses a non-descriptive error message.

 func TestRrEquals(t *testing.T) {
-    want := make([]dns.RR, 0)
-    rr, _ := dns.NewRR("jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
-    want = append(want, rr)
-    rr, _ = dns.NewRR("jain-bb.jain.ad.jp A   133.69.136.3")
-    want = append(want, rr)
+    tests := []struct {
+        name     string
+        records1 []string
+        records2 []string
+        want     bool
+    }{
+        {
+            name: "same records different order",
+            records1: []string{
+                "jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
+                "jain-bb.jain.ad.jp A   133.69.136.3",
+            },
+            records2: []string{
+                "jain-bb.jain.ad.jp A   133.69.136.3",
+                "jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
+            },
+            want: true,
+        },
+    }
 
-    otherWant := make([]dns.RR, 0)
-    rr, _ = dns.NewRR("jain-bb.jain.ad.jp A   133.69.136.3")
-    otherWant = append(otherWant, rr)
-    rr, _ = dns.NewRR("jain.ad.jp         SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
-    otherWant = append(otherWant, rr)
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            var want, otherWant []dns.RR
+            for _, r := range tt.records1 {
+                rr, err := dns.NewRR(r)
+                if err != nil {
+                    t.Fatalf("Failed to create RR: %v", err)
+                }
+                want = append(want, rr)
+            }
+            for _, r := range tt.records2 {
+                rr, err := dns.NewRR(r)
+                if err != nil {
+                    t.Fatalf("Failed to create RR: %v", err)
+                }
+                otherWant = append(otherWant, rr)
+            }
 
-    if !rrEquals(want, otherWant) {
-        t.Errorf("dns.RR Equality test failed!")
+            if got := rrEquals(want, otherWant); got != tt.want {
+                t.Errorf("rrEquals() = %v, want %v", got, tt.want)
+            }
+        })
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestRrEquals(t *testing.T) {
want := make([]dns.RR, 0)
rr, _ := dns.NewRR("jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
want = append(want, rr)
rr, _ = dns.NewRR("jain-bb.jain.ad.jp A 133.69.136.3")
want = append(want, rr)
otherWant := make([]dns.RR, 0)
rr, _ = dns.NewRR("jain-bb.jain.ad.jp A 133.69.136.3")
otherWant = append(otherWant, rr)
rr, _ = dns.NewRR("jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800")
otherWant = append(otherWant, rr)
if !rrEquals(want, otherWant) {
t.Errorf("dns.RR Equality test failed!")
}
}
func TestRrEquals(t *testing.T) {
tests := []struct {
name string
records1 []string
records2 []string
want bool
}{
{
name: "same records different order",
records1: []string{
"jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
"jain-bb.jain.ad.jp A 133.69.136.3",
},
records2: []string{
"jain-bb.jain.ad.jp A 133.69.136.3",
"jain.ad.jp SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. 3 600 600 3600000 604800",
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var want, otherWant []dns.RR
for _, r := range tt.records1 {
rr, err := dns.NewRR(r)
if err != nil {
t.Fatalf("Failed to create RR: %v", err)
}
want = append(want, rr)
}
for _, r := range tt.records2 {
rr, err := dns.NewRR(r)
if err != nil {
t.Fatalf("Failed to create RR: %v", err)
}
otherWant = append(otherWant, rr)
}
if got := rrEquals(want, otherWant); got != tt.want {
t.Errorf("rrEquals() = %v, want %v", got, tt.want)
}
})
}
}

Comment on lines +23 to +30
type Case struct {
Mqueue [][]string `yaml:"mqueue"`
Expected []string `yaml:"expected"`
Fetcher string `yaml:fetcher`
Checker string `yaml:checker`
Runs uint `yaml:runs`
Config map[string]string `yaml:config`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix YAML struct tag syntax.

The YAML struct tags are missing quotes, which makes them incompatible with reflect.StructTag.Get.

 type Case struct {
        Mqueue   [][]string        `yaml:"mqueue"`
        Expected []string          `yaml:"expected"`
-       Fetcher  string            `yaml:fetcher`
-       Checker  string            `yaml:checker`
-       Runs     uint              `yaml:runs`
-       Config   map[string]string `yaml:config`
+       Fetcher  string            `yaml:"fetcher"`
+       Checker  string            `yaml:"checker"`
+       Runs     uint              `yaml:"runs"`
+       Config   map[string]string `yaml:"config"`
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type Case struct {
Mqueue [][]string `yaml:"mqueue"`
Expected []string `yaml:"expected"`
Fetcher string `yaml:fetcher`
Checker string `yaml:checker`
Runs uint `yaml:runs`
Config map[string]string `yaml:config`
}
type Case struct {
Mqueue [][]string `yaml:"mqueue"`
Expected []string `yaml:"expected"`
Fetcher string `yaml:"fetcher"`
Checker string `yaml:"checker"`
Runs uint `yaml:"runs"`
Config map[string]string `yaml:"config"`
}
🧰 Tools
🪛 golangci-lint (1.62.2)

26-26: structtag: struct field tag yaml:fetcher not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)


27-27: structtag: struct field tag yaml:checker not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)


28-28: structtag: struct field tag yaml:runs not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)


29-29: structtag: struct field tag yaml:config not compatible with reflect.StructTag.Get: bad syntax for struct tag value

(govet)

Comment on lines +41 to +44
yaml.Unmarshal(file, &c)
if err != nil {
panic(fmt.Sprintf("Error unmarshaling testdata file: %s", err))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle YAML unmarshaling error.

The error from yaml.Unmarshal is not being checked. The current error check uses an undefined err variable.

Apply this diff to fix the error handling:

-yaml.Unmarshal(file, &c)
-if err != nil {
+if err := yaml.Unmarshal(file, &c); err != nil {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
yaml.Unmarshal(file, &c)
if err != nil {
panic(fmt.Sprintf("Error unmarshaling testdata file: %s", err))
}
if err := yaml.Unmarshal(file, &c); err != nil {
panic(fmt.Sprintf("Error unmarshaling testdata file: %s", err))
}

Comment on lines +101 to +106
if r == nil {
panic(fmt.Sprintf("*** error: %s\n", err))
}
if r.Rcode != dns.RcodeSuccess {
panic("Bad response code!")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling in response validation.

The current code uses panic for error handling, which is not idiomatic in Go tests. Use t.Fatal instead.

Apply this diff to improve error handling:

-if r == nil {
-	panic(fmt.Sprintf("*** error: %s\n", err))
-}
-if r.Rcode != dns.RcodeSuccess {
-	panic("Bad response code!")
-}
+if r == nil {
+	t.Fatalf("Failed to get response: %v", err)
+}
+if r.Rcode != dns.RcodeSuccess {
+	t.Fatalf("Bad response code: %v", r.Rcode)
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if r == nil {
panic(fmt.Sprintf("*** error: %s\n", err))
}
if r.Rcode != dns.RcodeSuccess {
panic("Bad response code!")
}
if r == nil {
t.Fatalf("Failed to get response: %v", err)
}
if r.Rcode != dns.RcodeSuccess {
t.Fatalf("Bad response code: %v", r.Rcode)
}

Comment on lines +77 to +91
for _ = range c.Runs {
/* Create the client */
client := new(dns.Client)
client.Net = "tcp"
var conn *dns.Conn

/* Retry 10 times in case StupiDNS is not up yet */
for range 10 {
conn, err = client.Dial(addr)

if err == nil {
break
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling for connection retries.

The retry loop doesn't handle the case where all retries fail, and it doesn't use a proper delay between retries.

Apply this diff to improve the retry mechanism:

-for range 10 {
+for i := 0; i < 10; i++ {
 	conn, err = client.Dial(addr)
 	if err == nil {
 		break
 	}
+	if i == 9 {
+		panic(fmt.Sprintf("Failed to connect after 10 retries: %v", err))
+	}
+	time.Sleep(100 * time.Millisecond)
}

Don't forget to add "time" to the imports.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1 to +14
# A basic test emulating a client getting two A records for a response and then
# extracts the IP addresses from that response.
mqueue:
- - "example.com. 190 IN A 23.192.228.84"
- "example.com. 300 IN A 96.7.128.175"
expected:
- "23.192.228.84"
- "96.7.128.175"
fetcher: "getFromBucket"
checker: "orderedSliceCompare"
runs: 1
config:
nameserver: "127.0.0.1"
port: "53534"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider reducing test duplication

This test file is nearly identical to ixfr_integration_two_records.yaml with only the port number being different. Consider:

  1. Creating a shared test data file for common configurations
  2. Documenting the distinction between "simple_request" and "ixfr_integration" test purposes
  3. Explaining the significance of different port numbers (53534 vs 53530)

Example shared structure:

# common_test_data.yaml
base_test:
  mqueue:
    - - "example.test. 190 IN A 23.192.228.84"
      - "example.test. 300 IN A 96.7.128.175"
  expected:
    - "23.192.228.84"
    - "96.7.128.175"
  fetcher: "getFromBucket"
  checker: "orderedSliceCompare"
  runs: 1
  config:
    nameserver: "127.0.0.1"

Then individual test files would only need to specify unique attributes and include the base configuration.

@johanix
Copy link
Owner

johanix commented Feb 3, 2025

I agree with the code rabbit. Good rabbit! :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants