loading up the forgejo repo on tangled to test page performance
1// Copyright 2022 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package integration
5
6import (
7 "errors"
8 "fmt"
9 "net/url"
10 "os"
11 "path/filepath"
12 "reflect"
13 "strings"
14 "testing"
15
16 auth_model "forgejo.org/models/auth"
17 repo_model "forgejo.org/models/repo"
18 "forgejo.org/models/unittest"
19 user_model "forgejo.org/models/user"
20 base "forgejo.org/modules/migration"
21 "forgejo.org/modules/setting"
22 "forgejo.org/modules/structs"
23 "forgejo.org/services/migrations"
24
25 "github.com/stretchr/testify/assert"
26 "github.com/stretchr/testify/require"
27 "gopkg.in/yaml.v3"
28)
29
30func TestDumpRestore(t *testing.T) {
31 onGiteaRun(t, func(t *testing.T, u *url.URL) {
32 AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
33 setting.Migrations.AllowLocalNetworks = true
34 AppVer := setting.AppVer
35 // Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
36 setting.AppVer = "1.16.0"
37 defer func() {
38 setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
39 setting.AppVer = AppVer
40 }()
41
42 require.NoError(t, migrations.Init())
43
44 reponame := "repo1"
45
46 basePath := t.TempDir()
47
48 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
49 repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
50 session := loginUser(t, repoOwner.Name)
51 token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadMisc)
52
53 //
54 // Phase 1: dump repo1 from the Gitea instance to the filesystem
55 //
56
57 ctx := t.Context()
58 opts := migrations.MigrateOptions{
59 GitServiceType: structs.GiteaService,
60 Issues: true,
61 PullRequests: true,
62 Labels: true,
63 Milestones: true,
64 Comments: true,
65 AuthToken: token,
66 CloneAddr: repo.CloneLink().HTTPS,
67 RepoName: reponame,
68 }
69 err := migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
70 require.NoError(t, err)
71
72 //
73 // Verify desired side effects of the dump
74 //
75 d := filepath.Join(basePath, repo.OwnerName, repo.Name)
76 for _, f := range []string{"repo.yml", "topic.yml", "label.yml", "milestone.yml", "issue.yml"} {
77 assert.FileExists(t, filepath.Join(d, f))
78 }
79
80 //
81 // Phase 2: restore from the filesystem to the Gitea instance in restoredrepo
82 //
83
84 newreponame := "restored"
85 err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{
86 "labels", "issues", "comments", "milestones", "pull_requests",
87 }, false)
88 require.NoError(t, err)
89
90 newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame})
91
92 //
93 // Phase 3: dump restored from the Gitea instance to the filesystem
94 //
95 opts.RepoName = newreponame
96 opts.CloneAddr = newrepo.CloneLink().HTTPS
97 err = migrations.DumpRepository(ctx, basePath, repoOwner.Name, opts)
98 require.NoError(t, err)
99
100 //
101 // Verify the dump of restored is the same as the dump of repo1
102 //
103 comparator := &compareDump{
104 t: t,
105 basePath: basePath,
106 }
107 comparator.assertEquals(repo, newrepo)
108 })
109}
110
111type compareDump struct {
112 t *testing.T
113 basePath string
114 repoBefore *repo_model.Repository
115 dirBefore string
116 repoAfter *repo_model.Repository
117 dirAfter string
118}
119
120type compareField struct {
121 before any
122 after any
123 ignore bool
124 transform func(string) string
125 nested *compareFields
126}
127
128type compareFields map[string]compareField
129
130func (c *compareDump) replaceRepoName(original string) string {
131 return strings.ReplaceAll(original, c.repoBefore.Name, c.repoAfter.Name)
132}
133
134func (c *compareDump) assertEquals(repoBefore, repoAfter *repo_model.Repository) {
135 c.repoBefore = repoBefore
136 c.dirBefore = filepath.Join(c.basePath, repoBefore.OwnerName, repoBefore.Name)
137 c.repoAfter = repoAfter
138 c.dirAfter = filepath.Join(c.basePath, repoAfter.OwnerName, repoAfter.Name)
139
140 //
141 // base.Repository
142 //
143 _ = c.assertEqual("repo.yml", base.Repository{}, compareFields{
144 "Name": {
145 before: c.repoBefore.Name,
146 after: c.repoAfter.Name,
147 },
148 "CloneURL": {transform: c.replaceRepoName},
149 "OriginalURL": {transform: c.replaceRepoName},
150 })
151
152 //
153 // base.Label
154 //
155 labels, ok := c.assertEqual("label.yml", []base.Label{}, compareFields{}).([]*base.Label)
156 assert.True(c.t, ok)
157 assert.GreaterOrEqual(c.t, len(labels), 1)
158
159 //
160 // base.Milestone
161 //
162 milestones, ok := c.assertEqual("milestone.yml", []base.Milestone{}, compareFields{
163 "Updated": {ignore: true}, // the database updates that field independently
164 }).([]*base.Milestone)
165 assert.True(c.t, ok)
166 assert.GreaterOrEqual(c.t, len(milestones), 1)
167
168 //
169 // base.Issue and the associated comments
170 //
171 issues, ok := c.assertEqual("issue.yml", []base.Issue{}, compareFields{
172 "Assignees": {ignore: true}, // not implemented yet
173 }).([]*base.Issue)
174 assert.True(c.t, ok)
175 assert.GreaterOrEqual(c.t, len(issues), 1)
176 for _, issue := range issues {
177 filename := filepath.Join("comments", fmt.Sprintf("%d.yml", issue.Number))
178 comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{
179 "Index": {ignore: true},
180 }).([]*base.Comment)
181 assert.True(c.t, ok)
182 for _, comment := range comments {
183 assert.Equal(c.t, issue.Number, comment.IssueIndex)
184 }
185 }
186
187 //
188 // base.PullRequest and the associated comments
189 //
190 comparePullRequestBranch := &compareFields{
191 "RepoName": {
192 before: c.repoBefore.Name,
193 after: c.repoAfter.Name,
194 },
195 "CloneURL": {transform: c.replaceRepoName},
196 }
197 prs, ok := c.assertEqual("pull_request.yml", []base.PullRequest{}, compareFields{
198 "Assignees": {ignore: true}, // not implemented yet
199 "Head": {nested: comparePullRequestBranch},
200 "Base": {nested: comparePullRequestBranch},
201 "Labels": {ignore: true}, // because org labels are not handled properly
202 }).([]*base.PullRequest)
203 assert.True(c.t, ok)
204 assert.GreaterOrEqual(c.t, len(prs), 1)
205 for _, pr := range prs {
206 filename := filepath.Join("comments", fmt.Sprintf("%d.yml", pr.Number))
207 comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{}).([]*base.Comment)
208 assert.True(c.t, ok)
209 for _, comment := range comments {
210 assert.Equal(c.t, pr.Number, comment.IssueIndex)
211 }
212 }
213}
214
215func (c *compareDump) assertLoadYAMLFiles(beforeFilename, afterFilename string, before, after any) {
216 _, beforeErr := os.Stat(beforeFilename)
217 _, afterErr := os.Stat(afterFilename)
218 assert.Equal(c.t, errors.Is(beforeErr, os.ErrNotExist), errors.Is(afterErr, os.ErrNotExist))
219 if errors.Is(beforeErr, os.ErrNotExist) {
220 return
221 }
222
223 beforeBytes, err := os.ReadFile(beforeFilename)
224 require.NoError(c.t, err)
225 require.NoError(c.t, yaml.Unmarshal(beforeBytes, before))
226 afterBytes, err := os.ReadFile(afterFilename)
227 require.NoError(c.t, err)
228 require.NoError(c.t, yaml.Unmarshal(afterBytes, after))
229}
230
231func (c *compareDump) assertLoadFiles(beforeFilename, afterFilename string, t reflect.Type) (before, after reflect.Value) {
232 var beforePtr, afterPtr reflect.Value
233 if t.Kind() == reflect.Slice {
234 //
235 // Given []Something{} create afterPtr, beforePtr []*Something{}
236 //
237 sliceType := reflect.SliceOf(reflect.PointerTo(t.Elem()))
238 beforeSlice := reflect.MakeSlice(sliceType, 0, 10)
239 beforePtr = reflect.New(beforeSlice.Type())
240 beforePtr.Elem().Set(beforeSlice)
241 afterSlice := reflect.MakeSlice(sliceType, 0, 10)
242 afterPtr = reflect.New(afterSlice.Type())
243 afterPtr.Elem().Set(afterSlice)
244 } else {
245 //
246 // Given Something{} create afterPtr, beforePtr *Something{}
247 //
248 beforePtr = reflect.New(t)
249 afterPtr = reflect.New(t)
250 }
251 c.assertLoadYAMLFiles(beforeFilename, afterFilename, beforePtr.Interface(), afterPtr.Interface())
252 return beforePtr.Elem(), afterPtr.Elem()
253}
254
255func (c *compareDump) assertEqual(filename string, kind any, fields compareFields) (i any) {
256 beforeFilename := filepath.Join(c.dirBefore, filename)
257 afterFilename := filepath.Join(c.dirAfter, filename)
258
259 typeOf := reflect.TypeOf(kind)
260 before, after := c.assertLoadFiles(beforeFilename, afterFilename, typeOf)
261 if typeOf.Kind() == reflect.Slice {
262 i = c.assertEqualSlices(before, after, fields)
263 } else {
264 i = c.assertEqualValues(before, after, fields)
265 }
266 return i
267}
268
269func (c *compareDump) assertEqualSlices(before, after reflect.Value, fields compareFields) any {
270 assert.Equal(c.t, before.Len(), after.Len())
271 if before.Len() == after.Len() {
272 for i := 0; i < before.Len(); i++ {
273 _ = c.assertEqualValues(
274 reflect.Indirect(before.Index(i).Elem()),
275 reflect.Indirect(after.Index(i).Elem()),
276 fields)
277 }
278 }
279 return after.Interface()
280}
281
282func (c *compareDump) assertEqualValues(before, after reflect.Value, fields compareFields) any {
283 for _, field := range reflect.VisibleFields(before.Type()) {
284 bf := before.FieldByName(field.Name)
285 bi := bf.Interface()
286 af := after.FieldByName(field.Name)
287 ai := af.Interface()
288 if compare, ok := fields[field.Name]; ok {
289 if compare.ignore == true {
290 //
291 // Ignore
292 //
293 continue
294 }
295 if compare.transform != nil {
296 //
297 // Transform these strings before comparing them
298 //
299 bs, ok := bi.(string)
300 assert.True(c.t, ok)
301 as, ok := ai.(string)
302 assert.True(c.t, ok)
303 assert.Equal(c.t, compare.transform(bs), compare.transform(as))
304 continue
305 }
306 if compare.before != nil && compare.after != nil {
307 //
308 // The fields are expected to have different values
309 //
310 assert.Equal(c.t, compare.before, bi)
311 assert.Equal(c.t, compare.after, ai)
312 continue
313 }
314 if compare.nested != nil {
315 //
316 // The fields are a struct, recurse
317 //
318 c.assertEqualValues(bf, af, *compare.nested)
319 continue
320 }
321 }
322 assert.Equal(c.t, bi, ai)
323 }
324 return after.Interface()
325}