16950: changes after review.
[arvados.git] / lib / costanalyzer / costanalyzer_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package costanalyzer
6
7 import (
8         "bytes"
9         "context"
10         "io"
11         "io/ioutil"
12         "os"
13         "regexp"
14         "testing"
15
16         "git.arvados.org/arvados.git/sdk/go/arvados"
17         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
18         "git.arvados.org/arvados.git/sdk/go/arvadostest"
19         "git.arvados.org/arvados.git/sdk/go/keepclient"
20         "gopkg.in/check.v1"
21 )
22
23 func Test(t *testing.T) {
24         check.TestingT(t)
25 }
26
27 var _ = check.Suite(&Suite{})
28
29 type Suite struct{}
30
31 func (s *Suite) TearDownSuite(c *check.C) {
32         // Undo any changes/additions to the database so they don't affect subsequent tests.
33         arvadostest.ResetEnv()
34 }
35
36 func (s *Suite) SetUpSuite(c *check.C) {
37         arvadostest.StartAPI()
38         arvadostest.StartKeep(2, true)
39
40         // Get the various arvados, arvadosclient, and keep client objects
41         ac := arvados.NewClientFromEnv()
42         arv, err := arvadosclient.MakeArvadosClient()
43         c.Assert(err, check.Equals, nil)
44         arv.ApiToken = arvadostest.ActiveToken
45         kc, err := keepclient.MakeKeepClient(arv)
46         c.Assert(err, check.Equals, nil)
47
48         standardE4sV3JSON := `{
49     "Name": "Standard_E4s_v3",
50     "ProviderType": "Standard_E4s_v3",
51     "VCPUs": 4,
52     "RAM": 34359738368,
53     "Scratch": 64000000000,
54     "IncludedScratch": 64000000000,
55     "AddedScratch": 0,
56     "Price": 0.292,
57     "Preemptible": false
58 }`
59         standardD32sV3JSON := `{
60     "Name": "Standard_D32s_v3",
61     "ProviderType": "Standard_D32s_v3",
62     "VCPUs": 32,
63     "RAM": 137438953472,
64     "Scratch": 256000000000,
65     "IncludedScratch": 256000000000,
66     "AddedScratch": 0,
67     "Price": 1.76,
68     "Preemptible": false
69 }`
70
71         standardA1V2JSON := `{
72     "Name": "a1v2",
73     "ProviderType": "Standard_A1_v2",
74     "VCPUs": 1,
75     "RAM": 2147483648,
76     "Scratch": 10000000000,
77     "IncludedScratch": 10000000000,
78     "AddedScratch": 0,
79     "Price": 0.043,
80     "Preemptible": false
81 }`
82
83         standardA2V2JSON := `{
84     "Name": "a2v2",
85     "ProviderType": "Standard_A2_v2",
86     "VCPUs": 2,
87     "RAM": 4294967296,
88     "Scratch": 20000000000,
89     "IncludedScratch": 20000000000,
90     "AddedScratch": 0,
91     "Price": 0.091,
92     "Preemptible": false
93 }`
94
95         // Our fixtures do not actually contain file contents. Populate the log collections we're going to use with the node.json file
96         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID, arvadostest.LogCollectionUUID, standardE4sV3JSON)
97         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID2, arvadostest.LogCollectionUUID2, standardD32sV3JSON)
98
99         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.DiagnosticsContainerRequest1LogCollectionUUID, standardA1V2JSON)
100         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest2UUID, arvadostest.DiagnosticsContainerRequest2LogCollectionUUID, standardA1V2JSON)
101         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher1ContainerRequestUUID, arvadostest.Hasher1LogCollectionUUID, standardA1V2JSON)
102         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher2ContainerRequestUUID, arvadostest.Hasher2LogCollectionUUID, standardA2V2JSON)
103         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher3ContainerRequestUUID, arvadostest.Hasher3LogCollectionUUID, standardA1V2JSON)
104 }
105
106 func createNodeJSON(c *check.C, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, crUUID string, logUUID string, nodeJSON string) {
107         // Get the CR
108         var cr arvados.ContainerRequest
109         err := ac.RequestAndDecodeContext(context.Background(), &cr, "GET", "arvados/v1/container_requests/"+crUUID, nil, nil)
110         c.Assert(err, check.Equals, nil)
111         c.Assert(cr.LogUUID, check.Equals, logUUID)
112
113         // Get the log collection
114         var coll arvados.Collection
115         err = ac.RequestAndDecodeContext(context.Background(), &coll, "GET", "arvados/v1/collections/"+cr.LogUUID, nil, nil)
116         c.Assert(err, check.IsNil)
117
118         // Create a node.json file -- the fixture doesn't actually contain the contents of the collection.
119         fs, err := coll.FileSystem(ac, kc)
120         c.Assert(err, check.IsNil)
121         f, err := fs.OpenFile("node.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
122         c.Assert(err, check.IsNil)
123         _, err = io.WriteString(f, nodeJSON)
124         c.Assert(err, check.IsNil)
125         err = f.Close()
126         c.Assert(err, check.IsNil)
127
128         // Flush the data to Keep
129         mtxt, err := fs.MarshalManifest(".")
130         c.Assert(err, check.IsNil)
131         c.Assert(mtxt, check.NotNil)
132
133         // Update collection record
134         err = ac.RequestAndDecodeContext(context.Background(), &coll, "PUT", "arvados/v1/collections/"+cr.LogUUID, nil, map[string]interface{}{
135                 "collection": map[string]interface{}{
136                         "manifest_text": mtxt,
137                 },
138         })
139         c.Assert(err, check.IsNil)
140 }
141
142 func (*Suite) TestUsage(c *check.C) {
143         var stdout, stderr bytes.Buffer
144         exitcode := Command.RunCommand("costanalyzer.test", []string{"-help", "-log-level=debug"}, &bytes.Buffer{}, &stdout, &stderr)
145         c.Check(exitcode, check.Equals, 1)
146         c.Check(stdout.String(), check.Equals, "")
147         c.Check(stderr.String(), check.Matches, `(?ms).*Usage:.*`)
148 }
149
150 func (*Suite) TestContainerRequestUUID(c *check.C) {
151         var stdout, stderr bytes.Buffer
152         // Run costanalyzer with 1 container request uuid
153         exitcode := Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.CompletedContainerRequestUUID}, &bytes.Buffer{}, &stdout, &stderr)
154         c.Check(exitcode, check.Equals, 0)
155         c.Assert(stdout.String(), check.Matches, "(?ms).*supplied uuids in .*")
156
157         uuidReport, err := ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID + ".csv")
158         c.Assert(err, check.IsNil)
159         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
160         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
161         matches := re.FindStringSubmatch(stdout.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
162
163         aggregateCostReport, err := ioutil.ReadFile(matches[1])
164         c.Assert(err, check.IsNil)
165
166         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
167 }
168
169 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
170         var stdout, stderr bytes.Buffer
171         // Run costanalyzer with 2 container request uuids
172         exitcode := Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.CompletedContainerRequestUUID, "-uuid", arvadostest.CompletedContainerRequestUUID2}, &bytes.Buffer{}, &stdout, &stderr)
173         c.Check(exitcode, check.Equals, 0)
174         c.Assert(stdout.String(), check.Matches, "(?ms).*supplied uuids in .*")
175
176         uuidReport, err := ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID + ".csv")
177         c.Assert(err, check.IsNil)
178         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
179
180         uuidReport2, err := ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
181         c.Assert(err, check.IsNil)
182         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
183
184         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
185         matches := re.FindStringSubmatch(stdout.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
186
187         aggregateCostReport, err := ioutil.ReadFile(matches[1])
188         c.Assert(err, check.IsNil)
189
190         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
191         stdout.Truncate(0)
192         stderr.Truncate(0)
193
194         // Now move both container requests into an existing project, and then re-run
195         // the analysis with the project uuid. The results should be identical.
196         ac := arvados.NewClientFromEnv()
197         var cr arvados.ContainerRequest
198         err = ac.RequestAndDecodeContext(context.Background(), &cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
199                 "container_request": map[string]interface{}{
200                         "owner_uuid": arvadostest.AProjectUUID,
201                 },
202         })
203         c.Assert(err, check.IsNil)
204         err = ac.RequestAndDecodeContext(context.Background(), &cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
205                 "container_request": map[string]interface{}{
206                         "owner_uuid": arvadostest.AProjectUUID,
207                 },
208         })
209         c.Assert(err, check.IsNil)
210
211         // Run costanalyzer with the project uuid
212         exitcode = Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.AProjectUUID, "-cache=false", "-log-level", "debug"}, &bytes.Buffer{}, &stdout, &stderr)
213         c.Check(exitcode, check.Equals, 0)
214         c.Assert(stdout.String(), check.Matches, "(?ms).*supplied uuids in .*")
215
216         uuidReport, err = ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID + ".csv")
217         c.Assert(err, check.IsNil)
218         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
219
220         uuidReport2, err = ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
221         c.Assert(err, check.IsNil)
222         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
223
224         re = regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
225         matches = re.FindStringSubmatch(stdout.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
226
227         aggregateCostReport, err = ioutil.ReadFile(matches[1])
228         c.Assert(err, check.IsNil)
229
230         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
231 }
232
233 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
234         var stdout, stderr bytes.Buffer
235         // Run costanalyzer with 2 container request uuids
236         exitcode := Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.CompletedDiagnosticsContainerRequest1UUID, "-uuid", arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
237         c.Check(exitcode, check.Equals, 0)
238         c.Assert(stdout.String(), check.Matches, "(?ms).*supplied uuids in .*")
239
240         uuidReport, err := ioutil.ReadFile("results/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
241         c.Assert(err, check.IsNil)
242         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00914539")
243
244         uuidReport2, err := ioutil.ReadFile("results/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
245         c.Assert(err, check.IsNil)
246         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00586435")
247
248         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
249         matches := re.FindStringSubmatch(stdout.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
250
251         aggregateCostReport, err := ioutil.ReadFile(matches[1])
252         c.Assert(err, check.IsNil)
253
254         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,0.01490377")
255 }