github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/email/lore/parse_test.go (about)

     1  // Copyright 2023 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package lore
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/google/syzkaller/dashboard/dashapi"
    14  	"github.com/google/syzkaller/pkg/email"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  func TestThreadsCollection(t *testing.T) {
    20  	messages := []string{
    21  		// <A-Base> <-- <A-Child-1> <-- <A-Child-1-1>.
    22  		`Date: Sun, 7 May 2017 19:54:00 -0700
    23  Subject: Thread A
    24  Message-ID: <A-Base>
    25  From: UserA <a@user.com>
    26  Content-Type: text/plain
    27  
    28  
    29  Some text`,
    30  		`Date: Sun, 7 May 2017 19:55:00 -0700
    31  Subject: Re: Thread A
    32  Message-ID: <A-Child-1>
    33  From: UserB <b@user.com>
    34  To: UserA <a@user.com>
    35  Content-Type: text/plain
    36  In-Reply-To: <A-Base>
    37  
    38  
    39  Some reply`,
    40  		`Date: Sun, 7 May 2017 19:56:00 -0700
    41  Subject: Re: Re: Thread A
    42  Message-ID: <A-Child-1-1>
    43  From: UserC <c@user.com>
    44  To: UserA <a@user.com>, UserB <b@user.com>
    45  Content-Type: text/plain
    46  In-Reply-To: <A-Child-1>
    47  
    48  
    49  Some reply (2)`,
    50  		// <Bug> with two children: <Bug-Reply1>, <Bug-Reply2>.
    51  		`Date: Sun, 7 May 2017 19:57:00 -0700
    52  Subject: [syzbot] Some bug
    53  Message-ID: <Bug>
    54  From: syzbot <syzbot+4564456@bar.com>
    55  Content-Type: text/plain
    56  
    57  
    58  Bug report`,
    59  		`Date: Sun, 7 May 2017 19:58:00 -0700
    60  Subject: Re: [syzbot] Some bug
    61  Message-ID: <Bug-Reply1>
    62  From: UserC <c@user.com>
    63  To: syzbot <syzbot+4564456@bar.com>
    64  In-Reply-To: <Bug>
    65  Content-Type: text/plain
    66  
    67  
    68  Bug report reply`,
    69  		`Date: Sun, 7 May 2017 19:58:01 -0700
    70  Subject: Re: [syzbot] Some bug
    71  Message-ID: <Bug-Reply2>
    72  From: UserD <d@user.com>
    73  To: syzbot <syzbot+4564456@bar.com>
    74  In-Reply-To: <Bug>B
    75  Content-Type: text/plain
    76  
    77  
    78  Bug report reply 2`,
    79  		// And one PATCH without replies.
    80  		`Date: Sun, 7 May 2017 19:58:01 -0700
    81  Subject: [PATCH] Some bug fixed
    82  Message-ID: <Patch>
    83  From: UserE <e@user.com>
    84  Cc: syzbot <syzbot+12345@bar.com>
    85  Content-Type: text/plain
    86  
    87  
    88  Patch`,
    89  		// An orphaned reply from a human.
    90  		`Date: Sun, 7 May 2017 19:57:00 -0700
    91  Subject: Another bug discussion
    92  In-Reply-To: <Unknown>
    93  Message-ID: <Sub-Discussion>
    94  From: person@email.com
    95  Cc: syzbot <syzbot+4564456@bar.com>
    96  Content-Type: text/plain
    97  
    98  
    99  Bug report`,
   100  		// An orphaned reply from a bot.
   101  		`Date: Sun, 7 May 2017 19:57:00 -0700
   102  Subject: Re: [syzbot] Some bug 3
   103  In-Reply-To: <Unknown>
   104  Message-ID: <Sub-Discussion-Bot>
   105  From: syzbot+4564456@bar.com
   106  To: all@email.com
   107  Content-Type: text/plain
   108  
   109  
   110  Bug report`,
   111  	}
   112  
   113  	zone := time.FixedZone("", -7*60*60)
   114  	expected := map[string]*Thread{
   115  		"<A-Base>": {
   116  			Subject:   "Thread A",
   117  			MessageID: "<A-Base>",
   118  			Type:      dashapi.DiscussionMention,
   119  			Messages: []*Email{
   120  				{
   121  					Email: &email.Email{
   122  						MessageID: "<A-Base>",
   123  						Subject:   "Thread A",
   124  						Date:      time.Date(2017, time.May, 7, 19, 54, 0, 0, zone),
   125  						Author:    "a@user.com",
   126  						Cc:        []string{"a@user.com"},
   127  					},
   128  				},
   129  				{
   130  					Email: &email.Email{
   131  						MessageID: "<A-Child-1>",
   132  						Subject:   "Re: Thread A",
   133  						Date:      time.Date(2017, time.May, 7, 19, 55, 0, 0, zone),
   134  						Author:    "b@user.com",
   135  						Cc:        []string{"a@user.com", "b@user.com"},
   136  						InReplyTo: "<A-Base>",
   137  					},
   138  				},
   139  				{
   140  					Email: &email.Email{
   141  						MessageID: "<A-Child-1-1>",
   142  						Subject:   "Re: Re: Thread A",
   143  						Date:      time.Date(2017, time.May, 7, 19, 56, 0, 0, zone),
   144  						Author:    "c@user.com",
   145  						Cc:        []string{"a@user.com", "b@user.com", "c@user.com"},
   146  						InReplyTo: "<A-Child-1>",
   147  					},
   148  				},
   149  			},
   150  		},
   151  		"<Bug>": {
   152  			Subject:   "[syzbot] Some bug",
   153  			MessageID: "<Bug>",
   154  			Type:      dashapi.DiscussionReport,
   155  			BugIDs:    []string{"4564456"},
   156  			Messages: []*Email{
   157  				{
   158  					Email: &email.Email{
   159  						MessageID: "<Bug>",
   160  						BugIDs:    []string{"4564456"},
   161  						Subject:   "[syzbot] Some bug",
   162  						Date:      time.Date(2017, time.May, 7, 19, 57, 0, 0, zone),
   163  						Author:    "syzbot@bar.com",
   164  						OwnEmail:  true,
   165  					},
   166  				},
   167  				{
   168  					Email: &email.Email{
   169  						MessageID: "<Bug-Reply1>",
   170  						BugIDs:    []string{"4564456"},
   171  						Subject:   "Re: [syzbot] Some bug",
   172  						Date:      time.Date(2017, time.May, 7, 19, 58, 0, 0, zone),
   173  						Author:    "c@user.com",
   174  						Cc:        []string{"c@user.com"},
   175  						InReplyTo: "<Bug>",
   176  					},
   177  				},
   178  				{
   179  					Email: &email.Email{
   180  						MessageID: "<Bug-Reply2>",
   181  						BugIDs:    []string{"4564456"},
   182  						Subject:   "Re: [syzbot] Some bug",
   183  						Date:      time.Date(2017, time.May, 7, 19, 58, 1, 0, zone),
   184  						Author:    "d@user.com",
   185  						Cc:        []string{"d@user.com"},
   186  						InReplyTo: "<Bug>",
   187  					},
   188  				},
   189  			},
   190  		},
   191  		"<Patch>": {
   192  			Subject:   "[PATCH] Some bug fixed",
   193  			MessageID: "<Patch>",
   194  			Type:      dashapi.DiscussionPatch,
   195  			BugIDs:    []string{"12345"},
   196  			Messages: []*Email{
   197  				{
   198  					Email: &email.Email{
   199  						MessageID: "<Patch>",
   200  						BugIDs:    []string{"12345"},
   201  						Subject:   "[PATCH] Some bug fixed",
   202  						Date:      time.Date(2017, time.May, 7, 19, 58, 1, 0, zone),
   203  						Author:    "e@user.com",
   204  						Cc:        []string{"e@user.com"},
   205  					},
   206  				},
   207  			},
   208  		},
   209  		"<Sub-Discussion>": {
   210  			Subject:   "Another bug discussion",
   211  			MessageID: "<Sub-Discussion>",
   212  			Type:      dashapi.DiscussionMention,
   213  			BugIDs:    []string{"4564456"},
   214  			Messages: []*Email{
   215  				{
   216  					Email: &email.Email{
   217  						MessageID: "<Sub-Discussion>",
   218  						InReplyTo: "<Unknown>",
   219  						Date:      time.Date(2017, time.May, 7, 19, 57, 0, 0, zone),
   220  						BugIDs:    []string{"4564456"},
   221  						Cc:        []string{"person@email.com"},
   222  						Subject:   "Another bug discussion",
   223  						Author:    "person@email.com",
   224  					},
   225  				},
   226  			},
   227  		},
   228  		"<Sub-Discussion-Bot>": nil,
   229  	}
   230  
   231  	var emails []*Email
   232  	for _, m := range messages {
   233  		msg, err := emailFromRaw([]byte(m), []string{"syzbot@bar.com"}, []string{"bar.com"})
   234  		if err != nil {
   235  			t.Fatal(err)
   236  		}
   237  		msg.RawCc = nil
   238  		emails = append(emails, msg)
   239  	}
   240  
   241  	threads := Threads(emails)
   242  	got := map[string]*Thread{}
   243  
   244  	for _, d := range threads {
   245  		sort.Slice(d.Messages, func(i, j int) bool {
   246  			return d.Messages[i].Date.Before(d.Messages[j].Date)
   247  		})
   248  		got[d.MessageID] = d
   249  	}
   250  
   251  	for key, val := range expected {
   252  		if diff := cmp.Diff(val, got[key]); diff != "" {
   253  			t.Fatalf("%s: %s", key, diff)
   254  		}
   255  	}
   256  
   257  	if len(threads) > len(expected) {
   258  		t.Fatalf("expected %d threads, got %d", len(expected), len(threads))
   259  	}
   260  }
   261  
   262  func TestParsePatchSubject(t *testing.T) {
   263  	tests := []struct {
   264  		subj string
   265  		ret  PatchSubject
   266  	}{
   267  		{
   268  			subj: `[PATCH] abcd`,
   269  			ret:  PatchSubject{Title: "abcd"},
   270  		},
   271  		{
   272  			subj: `[PATCH 00/20] abcd`,
   273  			ret:  PatchSubject{Title: "abcd", Seq: value[int](0), Total: value[int](20)},
   274  		},
   275  		{
   276  			subj: `[PATCH 5/6] abcd`,
   277  			ret:  PatchSubject{Title: "abcd", Seq: value[int](5), Total: value[int](6)},
   278  		},
   279  		{
   280  			subj: `[PATCH RFC v3 0/4] abcd`,
   281  			ret: PatchSubject{
   282  				Title:   "abcd",
   283  				Tags:    []string{"RFC"},
   284  				Version: value[int](3),
   285  				Seq:     value[int](0),
   286  				Total:   value[int](4),
   287  			},
   288  		},
   289  		{
   290  			subj: `[RFC PATCH] abcd`,
   291  			ret:  PatchSubject{Title: "abcd", Tags: []string{"RFC"}},
   292  		},
   293  		{
   294  			subj: `[PATCH net-next v2 00/21] abcd`,
   295  			ret: PatchSubject{
   296  				Title:   "abcd",
   297  				Tags:    []string{"net-next"},
   298  				Version: value[int](2),
   299  				Seq:     value[int](0),
   300  				Total:   value[int](21),
   301  			},
   302  		},
   303  		{
   304  			subj: `[PATCH v2 RESEND] abcd`,
   305  			ret:  PatchSubject{Title: "abcd", Version: value[int](2), Tags: []string{"RESEND"}},
   306  		},
   307  		{
   308  			subj: `[PATCH RFC net-next v3 05/21] abcd`,
   309  			ret: PatchSubject{
   310  				Title:   "abcd",
   311  				Tags:    []string{"RFC", "net-next"},
   312  				Version: value[int](3),
   313  				Seq:     value[int](5),
   314  				Total:   value[int](21),
   315  			},
   316  		},
   317  	}
   318  	for id, test := range tests {
   319  		t.Run(fmt.Sprint(id), func(t *testing.T) {
   320  			ret, ok := parsePatchSubject(test.subj)
   321  			assert.True(t, ok)
   322  			assert.Equal(t, test.ret, ret)
   323  		})
   324  	}
   325  }
   326  
   327  func TestDiscussionType(t *testing.T) {
   328  	tests := []struct {
   329  		msg *email.Email
   330  		ret dashapi.DiscussionType
   331  	}{
   332  		{
   333  			msg: &email.Email{
   334  				Subject: "[PATCH] Bla-bla",
   335  			},
   336  			ret: dashapi.DiscussionPatch,
   337  		},
   338  		{
   339  			msg: &email.Email{
   340  				Subject: "[patch v3] Bla-bla",
   341  			},
   342  			ret: dashapi.DiscussionPatch,
   343  		},
   344  		{
   345  			msg: &email.Email{
   346  				Subject: "[RFC PATCH] Bla-bla",
   347  			},
   348  			ret: dashapi.DiscussionPatch,
   349  		},
   350  		{
   351  			msg: &email.Email{
   352  				Subject: "[RESEND PATCH] Bla-bla",
   353  			},
   354  			ret: dashapi.DiscussionPatch,
   355  		},
   356  		{
   357  			msg: &email.Email{
   358  				Subject:  "[syzbot] Monthly ext4 report",
   359  				OwnEmail: true,
   360  			},
   361  			ret: dashapi.DiscussionReminder,
   362  		},
   363  		{
   364  			msg: &email.Email{
   365  				Subject:  "[syzbot] WARNING in abcd",
   366  				OwnEmail: true,
   367  			},
   368  			ret: dashapi.DiscussionReport,
   369  		},
   370  		{
   371  			msg: &email.Email{
   372  				Subject: "Some human-reported bug",
   373  			},
   374  			ret: dashapi.DiscussionMention,
   375  		},
   376  	}
   377  	for _, test := range tests {
   378  		got := DiscussionType(test.msg)
   379  		if got != test.ret {
   380  			t.Fatalf("expected %v got %v for %v", test.ret, got, test.msg)
   381  		}
   382  	}
   383  }
   384  
   385  const dummyPatch = `diff --git a/kernel/kcov.c b/kernel/kcov.c
   386  index 85e5546cd791..949ea4574412 100644
   387  --- a/kernel/kcov.c
   388  +++ b/kernel/kcov.c
   389  @@ -127,7 +127,6 @@ void kcov_task_exit(struct task_struct *t)
   390   	if (kcov == NULL)
   391   		return;
   392  -	spin_lock(&kcov->lock);
   393   	if (WARN_ON(kcov->t != t)) {
   394  `
   395  
   396  func TestParseSeries(t *testing.T) {
   397  	messages := []string{
   398  		// A simple patch series.
   399  		`Date: Sun, 7 May 2017 19:54:00 -0700
   400  Subject: [PATCH] Small patch
   401  Message-ID: <First>
   402  From: UserA <a@user.com>
   403  Content-Type: text/plain
   404  
   405  ` + dummyPatch,
   406  		// A series with a cover.
   407  		`Date: Sun, 7 May 2017 19:55:00 -0700
   408  Subject: [PATCH net v2 00/02] A longer series
   409  Message-ID: <Second>
   410  From: UserB <b@user.com>
   411  To: UserA <a@user.com>
   412  Content-Type: text/plain
   413  
   414  Some cover`,
   415  		`Date: Sun, 7 May 2017 19:56:00 -0700
   416  Subject: [PATCH net v2 01/02] First patch
   417  Message-ID: <Second-1>
   418  From: UserC <c@user.com>
   419  To: UserA <a@user.com>, UserB <b@user.com>
   420  Content-Type: text/plain
   421  In-Reply-To: <Second>
   422  
   423  ` + dummyPatch,
   424  		`Date: Sun, 7 May 2017 19:56:00 -0700
   425  Subject: [PATCH net v2 02/02] Second patch
   426  Message-ID: <Second-2>
   427  From: UserC <c@user.com>
   428  To: UserA <a@user.com>, UserB <b@user.com>
   429  Content-Type: text/plain
   430  In-Reply-To: <Second>
   431  
   432  ` + dummyPatch,
   433  		// Some missing patches.
   434  		`Date: Sun, 7 May 2017 19:57:00 -0700
   435  Subject: [PATCH 01/03] Series
   436  Message-ID: <Third>
   437  From: Someone <a@b.com>
   438  Content-Type: text/plain
   439  
   440  ` + dummyPatch,
   441  		// Reply with a patch subject.
   442  		`Date: Sun, 7 May 2017 19:57:00 -0700
   443  Subject: [PATCH] Series
   444  Message-ID: <Fourth>
   445  From: Someone <a@b.com>
   446  Content-Type: text/plain
   447  In-Reply-To: <Something>
   448  
   449  No patch, just text`,
   450  	}
   451  
   452  	var emails []*Email
   453  	for _, m := range messages {
   454  		msg, err := emailFromRaw([]byte(m), nil, nil)
   455  		if err != nil {
   456  			t.Fatal(err)
   457  		}
   458  		emails = append(emails, msg)
   459  	}
   460  
   461  	series := PatchSeries(emails)
   462  	assert.Len(t, series, 4)
   463  
   464  	expectPerID := map[string]*Series{
   465  		"<First>": {
   466  			Subject: "Small patch",
   467  			Version: 1,
   468  			Patches: []Patch{
   469  				{
   470  					Seq:   1,
   471  					Email: &Email{Email: &email.Email{Subject: "[PATCH] Small patch"}},
   472  				},
   473  			},
   474  		},
   475  		"<Second>": {
   476  			Subject: "A longer series",
   477  			Version: 2,
   478  			Tags:    []string{"net"},
   479  			Patches: []Patch{
   480  				{
   481  					Seq:   1,
   482  					Email: &Email{Email: &email.Email{Subject: "[PATCH v2 01/02] First patch"}},
   483  				},
   484  				{
   485  					Seq:   2,
   486  					Email: &Email{Email: &email.Email{Subject: "[PATCH v2 02/02] Second patch"}},
   487  				},
   488  			},
   489  		},
   490  		"<Third>": {
   491  			Subject:   "Series",
   492  			Version:   1,
   493  			Corrupted: "the subject mentions 3 patches, 1 are found",
   494  			Patches: []Patch{
   495  				{
   496  					Seq:   1,
   497  					Email: &Email{Email: &email.Email{Subject: "[PATCH 01/03] Series"}},
   498  				},
   499  			},
   500  		},
   501  		"<Fourth>": {
   502  			Subject:   "Series",
   503  			Version:   1,
   504  			Corrupted: "the subject mentions 1 patches, 0 are found",
   505  			Patches:   nil,
   506  		},
   507  	}
   508  	for _, s := range series {
   509  		expect := expectPerID[s.MessageID]
   510  		if expect == nil {
   511  			t.Fatalf("unexpected message: %q", s.MessageID)
   512  		}
   513  		expectPerID[s.MessageID] = nil
   514  		t.Run(s.MessageID, func(t *testing.T) {
   515  			assert.Equal(t, expect.Corrupted, s.Corrupted, "corrupted differs")
   516  			assert.Equal(t, expect.Subject, s.Subject, "subject differs")
   517  			assert.Equal(t, expect.Version, s.Version, "version differs")
   518  			require.Len(t, s.Patches, len(expect.Patches), "patch count differs")
   519  			for i, expectPatch := range expect.Patches {
   520  				got := s.Patches[i]
   521  				assert.Equal(t, expectPatch.Seq, got.Seq, "seq differs")
   522  			}
   523  		})
   524  	}
   525  }