Skip to content

Commit 5e32cad

Browse files
authored
Merge pull request #54 from icann/ft-force_aggregate
add --force-date option to aggregate
2 parents a0fb216 + 3fbb2ac commit 5e32cad

File tree

8 files changed

+162
-19
lines changed

8 files changed

+162
-19
lines changed

app/cmd/aggregate.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cmd
55
import (
66
"dnsmag/internal"
77
"fmt"
8+
"time"
89

910
"github.com/spf13/cobra"
1011
)
@@ -22,17 +23,19 @@ func newAggregateCmd() *cobra.Command {
2223
timing := internal.NewTimingStats()
2324

2425
var (
25-
top int
26-
verbose bool
27-
quiet bool
28-
output string
26+
top int
27+
verbose bool
28+
quiet bool
29+
output string
30+
forceDate string
2931
)
3032

3133
parseFlags(cmd, map[string]any{
32-
"top": &top,
33-
"verbose": &verbose,
34-
"quiet": &quiet,
35-
"output": &output,
34+
"top": &top,
35+
"verbose": &verbose,
36+
"quiet": &quiet,
37+
"output": &output,
38+
"force-date": &forceDate,
3639
})
3740

3841
// Quiet and verbose flags are mutually exclusive
@@ -41,7 +44,17 @@ func newAggregateCmd() *cobra.Command {
4144
return fmt.Errorf("conflicting flags: cannot use both --quiet and --verbose")
4245
}
4346

44-
seq := internal.NewDatasetSequence(top, nil)
47+
var forcedDate *time.Time
48+
if forceDate != "" {
49+
parsedDate, err := time.Parse("2006-01-02", forceDate)
50+
if err != nil {
51+
cmd.SilenceUsage = true
52+
return fmt.Errorf("invalid date format for --force-date: %s (expected YYYY-MM-DD)", forceDate)
53+
}
54+
forcedDate = &parsedDate
55+
}
56+
57+
seq := internal.NewDatasetSequence(top, forcedDate, forcedDate != nil, stderr)
4558

4659
// Load all provided DNSMAG files
4760
err := loadDatasets(cmd, seq, args, verbose)
@@ -102,6 +115,7 @@ func newAggregateCmd() *cobra.Command {
102115
aggregateCmd.Flags().IntP("top", "n", internal.DefaultDomainCount, "Minimum number of domains required in each dataset")
103116
aggregateCmd.Flags().BoolP("verbose", "v", false, "Verbose output")
104117
aggregateCmd.Flags().BoolP("quiet", "q", false, "Quiet mode")
118+
aggregateCmd.Flags().String("force-date", "", "Force a specific date for the aggregated dataset (YYYY-MM-DD format)")
105119

106120
return aggregateCmd
107121
}

app/cmd/aggregate_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,89 @@ func (sr *slowReader) Read(out []byte) (n int, err error) {
209209

210210
return numBytes, nil
211211
}
212+
213+
func TestAggregateCmd_DifferentDates_WithoutForceDate(t *testing.T) {
214+
file1, file2, cleanup := createDNSMagFilesWithDifferentDates(t, "2024-04-04", "2025-05-05")
215+
defer cleanup()
216+
217+
// Try to aggregate the two DNSMAG files without --force-date
218+
aggregateCmd := newAggregateCmd()
219+
aggregateCmd.SetArgs([]string{
220+
file1,
221+
file2,
222+
})
223+
224+
var aggregateBuf bytes.Buffer
225+
aggregateCmd.SetOut(&aggregateBuf)
226+
aggregateCmd.SetErr(&aggregateBuf)
227+
228+
err := aggregateCmd.Execute()
229+
if err == nil {
230+
t.Fatalf("Expected error when aggregating datasets with different dates, but got none. Output: %s", aggregateBuf.String())
231+
}
232+
233+
// Verify the error message mentions date mismatch
234+
output := aggregateBuf.String()
235+
if !regexp.MustCompile(`date mismatch`).MatchString(err.Error()) {
236+
t.Errorf("Expected 'date mismatch' error, got: %v\nOutput: %s", err, output)
237+
}
238+
239+
t.Logf("Expected error occurred: %v", err)
240+
}
241+
242+
func TestAggregateCmd_DifferentDates_WithForceDate(t *testing.T) {
243+
file1, file2, cleanup := createDNSMagFilesWithDifferentDates(t, "2024-04-04", "2025-05-05")
244+
defer cleanup()
245+
246+
// Aggregate the two DNSMAG files WITH --force-date
247+
aggregateCmd := newAggregateCmd()
248+
aggregateCmd.SetArgs([]string{
249+
"--force-date", "2026-01-14",
250+
file1,
251+
file2,
252+
})
253+
254+
var aggregateBuf bytes.Buffer
255+
aggregateCmd.SetOut(&aggregateBuf)
256+
aggregateCmd.SetErr(&aggregateBuf)
257+
258+
err := aggregateCmd.Execute()
259+
if err != nil {
260+
t.Fatalf("Aggregate command with --force-date failed: %v\nOutput: %s", err, aggregateBuf.String())
261+
}
262+
263+
output := aggregateBuf.String()
264+
265+
// Verify the output contains expected patterns
266+
expectedPatterns := []*regexp.Regexp{
267+
regexp.MustCompile(`Aggregated statistics for 2 datasets:`),
268+
regexp.MustCompile(`Date\s+:\s+2026-01-14`), // The forced date
269+
regexp.MustCompile(`Total queries\s+:\s+40`), // 25 + 15
270+
regexp.MustCompile(`Total domains\s+:\s+2`), // com, org
271+
regexp.MustCompile(`Warning: Overriding date 2025-05-05 with forced date 2026-01-14`), // Warning message
272+
}
273+
274+
for _, pattern := range expectedPatterns {
275+
if !pattern.MatchString(output) {
276+
t.Errorf("Expected pattern %q not found in output:\n%s", pattern.String(), output)
277+
}
278+
}
279+
280+
t.Logf("Aggregate command with --force-date output:\n%s", output)
281+
}
282+
283+
// createDNSMagFilesWithDifferentDates creates two temporary DNSMAG files with different dates
284+
// Returns the two file paths and a cleanup function
285+
func createDNSMagFilesWithDifferentDates(t *testing.T, date1, date2 string) (string, string, func()) {
286+
t.Helper()
287+
288+
file1 := createDNSMAGFromCSV(t, "192.168.1.1,example.com,25", date1)
289+
file2 := createDNSMAGFromCSV(t, "10.0.1.1,example.org,15", date2)
290+
291+
cleanup := func() {
292+
os.Remove(file1)
293+
os.Remove(file2)
294+
}
295+
296+
return file1, file2, cleanup
297+
}

app/cmd/collect.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
func newCollectCmd() *cobra.Command {
1414
collectCmd := &cobra.Command{
1515
Use: "collect <input-file> [input-file2] [input-file3...]",
16-
Short: "Parse PCAP files and generate domain statistics",
16+
Short: "Parse PCAP or CSV files and generate domain statistics",
1717
Long: `Parse one or more PCAP files containing DNS traffic and generate domain statistics.
1818
Save them to a DNSMAG file (CBOR format).`,
1919
Args: cobra.MinimumNArgs(1),

app/cmd/common_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"dnsmag/internal"
66
"fmt"
7+
"os"
78
"regexp"
89
"strings"
910
"testing"
@@ -72,3 +73,30 @@ func loadDatasetFromCSV(csvData string, dateStr string, verbose bool) (*internal
7273

7374
return collector, nil
7475
}
76+
77+
// createDNSMAGFromCSV creates a temporary DNSMAG file with the specified date and CSV data
78+
// Returns the file path. Caller is responsible for deleting the file.
79+
func createDNSMAGFromCSV(t *testing.T, csvData, date string) string {
80+
t.Helper()
81+
82+
tmpFile, err := os.CreateTemp("", "test_*.dnsmag")
83+
if err != nil {
84+
t.Fatalf("Failed to create temp DNSMAG file: %v", err)
85+
}
86+
filePath := tmpFile.Name()
87+
tmpFile.Close()
88+
89+
collector, err := loadDatasetFromCSV(csvData, date, false)
90+
if err != nil {
91+
os.Remove(filePath)
92+
t.Fatalf("Failed to load CSV dataset: %v", err)
93+
}
94+
95+
_, err = internal.WriteDNSMagFile(collector.Result, filePath, os.Stdout)
96+
if err != nil {
97+
os.Remove(filePath)
98+
t.Fatalf("Failed to write DNSMAG file: %v", err)
99+
}
100+
101+
return filePath
102+
}

app/cmd/report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func newReportCmd() *cobra.Command {
4747
"verbose": &verbose,
4848
})
4949

50-
seq := internal.NewDatasetSequence(0, nil)
50+
seq := internal.NewDatasetSequence(0, nil, false, stderr)
5151

5252
if err := loadDatasets(cmd, seq, []string{filename}, verbose); err != nil {
5353
cmd.SilenceUsage = true

app/cmd/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func newViewCmd() *cobra.Command {
4343

4444
cmd.SilenceUsage = true
4545

46-
seq := internal.NewDatasetSequence(top, nil)
46+
seq := internal.NewDatasetSequence(top, nil, false, stderr)
4747

4848
if err := loadDatasets(cmd, seq, []string{inputFile}, verbose); err != nil {
4949
return err

internal/store.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,17 @@ type DatasetSequence struct {
9191
numDomains int
9292
Count int
9393
Result MagnitudeDataset
94+
forceDate bool
95+
logger io.Writer
9496
}
9597

96-
func NewDatasetSequence(numDomains int, date *time.Time) *DatasetSequence {
98+
func NewDatasetSequence(numDomains int, date *time.Time, forceDate bool, logger io.Writer) *DatasetSequence {
9799
return &DatasetSequence{
98100
numDomains: numDomains,
99101
Count: 0,
100102
Result: newDataset(date),
103+
forceDate: forceDate,
104+
logger: logger,
101105
}
102106
}
103107

@@ -168,6 +172,17 @@ func (seq *DatasetSequence) LoadDNSMagSequenceFromReader(reader io.Reader, filen
168172
}
169173

170174
func (seq *DatasetSequence) addDataset(dataset MagnitudeDataset) error {
175+
// If forceDate is true and the dataset has a different date, log a warning and override it
176+
if seq.forceDate && dataset.Date != nil && seq.Result.Date != nil {
177+
if dataset.DateString() != seq.Result.DateString() {
178+
if seq.logger != nil {
179+
fmt.Fprintf(seq.logger, "Warning: Overriding date %s with forced date %s for dataset %s\n",
180+
dataset.DateString(), seq.Result.DateString(), dataset.extraSourceFilename)
181+
}
182+
dataset.SetDate(&seq.Result.Date.Time)
183+
}
184+
}
185+
171186
if seq.Count == 0 {
172187
seq.Result = dataset
173188
seq.Count = 1

internal/store_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestWriteAndLoadDNSMagFile_WriteLoadCycle(t *testing.T) {
4646
}
4747

4848
// Test loading
49-
seq := NewDatasetSequence(0, nil)
49+
seq := NewDatasetSequence(0, nil, false, nil)
5050
err = seq.LoadDNSMagFile(tmpFile.Name())
5151
if err != nil {
5252
t.Fatalf("LoadDNSMagFile failed: %v", err)
@@ -90,7 +90,7 @@ func TestWriteDNSMagFile_CreateError(t *testing.T) {
9090
}
9191

9292
func TestLoadDNSMagFile_FileNotFound(t *testing.T) {
93-
seq := NewDatasetSequence(0, nil)
93+
seq := NewDatasetSequence(0, nil, false, nil)
9494
err := seq.LoadDNSMagFile("non-existent.dnsmag")
9595
if err == nil {
9696
t.Error("Expected error when loading non-existent file, got nil")
@@ -111,7 +111,7 @@ func TestLoadDNSMagFile_InvalidFormat(t *testing.T) {
111111
}
112112
tmpFile.Close()
113113

114-
seq := NewDatasetSequence(0, nil)
114+
seq := NewDatasetSequence(0, nil, false, nil)
115115
err = seq.LoadDNSMagFile(tmpFile.Name())
116116
if err == nil {
117117
t.Error("Expected error when loading invalid CBOR file, got nil")
@@ -262,7 +262,7 @@ func TestWriteAndLoadDNSMagSequence_MultiDataset(t *testing.T) {
262262
tmpFile.Close()
263263

264264
// Load the two datasets from the single file
265-
seq := NewDatasetSequence(100, &dataset1.Date.Time)
265+
seq := NewDatasetSequence(100, &dataset1.Date.Time, false, nil)
266266
err = seq.LoadDNSMagSequenceFromReaderFile(tmpFile.Name())
267267
if err != nil {
268268
t.Fatalf("LoadDNSMagSequenceFromReaderFile failed: %v", err)
@@ -318,7 +318,7 @@ func TestLoadDNSMagSequenceFromReader_ExtraBytes(t *testing.T) {
318318
extra := []byte("EXTRA BYTES")
319319
input := append(cborData, extra...)
320320

321-
seq := NewDatasetSequence(100, &dataset.Date.Time)
321+
seq := NewDatasetSequence(100, &dataset.Date.Time, false, nil)
322322
err = seq.LoadDNSMagSequenceFromReader(
323323
bytes.NewReader(input),
324324
"testfile#%d",
@@ -347,7 +347,7 @@ func TestLoadDNSMagSequenceFromReader_IncompleteCBOR(t *testing.T) {
347347
// Truncate the CBOR data to simulate incomplete input
348348
truncated := cborData[:len(cborData)-1]
349349

350-
seq := NewDatasetSequence(100, &dataset.Date.Time)
350+
seq := NewDatasetSequence(100, &dataset.Date.Time, false, nil)
351351
err = seq.LoadDNSMagSequenceFromReader(
352352
bytes.NewReader(truncated),
353353
"testfile#%d",

0 commit comments

Comments
 (0)