Skip to content

Commit 6890a98

Browse files
committed
reporter: add unit tests for the usage report payload and missing licenses
Pin down the JSON that the management server POSTs to usage.report.uri so any change to the wire format has to be a deliberate one. UsageReporterTest mocks the seven DAOs the report is built from and drives the real report builders through the real AtomicGsonAdapter, comparing the result against a checked-in fixture, usage-report-expected.json. Keys are sorted on both sides before comparing: AtomicLongMap is backed by a ConcurrentHashMap and the report itself by a HashMap, so key order on the wire is not deterministic and must not be part of the contract. Sorting also makes a mismatch print a readable diff. The tests document three things that are not obvious from reading the code: - provisioning_type keys are lowercase ("thin"/"fat"). Storage.Provisio- ningType overrides toString() and the adapter keys on String.valueOf(), so the payload does not carry the enum constant names that Gson would emit by default. - boolean counters reach the wire as the string keys "true" and "false", used by ha_enabled, dynamically_scalable, compute_only and use_local_storage. - a report from an empty install still carries all eight sections with empty counter objects, and avg_disk_size falls back to 0 rather than dividing by zero. AtomicGsonAdapterTest covers the adapter on its own: null and empty maps, counts as numbers, boolean and enum keys, and that read() consumes a null. Also add the ASF license headers that apache-rat flags on reporter/README.md and reporter/requirements.txt.
1 parent eeba878 commit 6890a98

5 files changed

Lines changed: 823 additions & 0 deletions

File tree

reporter/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
120
# CloudStack Usage Reporter
221

322
This directory contains the server-side webservice for the Apache CloudStack usage reporting feature. When enabled, CloudStack management servers periodically send an anonymized report to the Apache CloudStack project. This data helps the community understand how CloudStack is deployed and used in the field.

reporter/requirements.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
118
flask>=2.2,<4
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.report;
18+
19+
import java.io.IOException;
20+
import java.io.StringReader;
21+
import java.io.StringWriter;
22+
23+
import org.junit.Assert;
24+
import org.junit.Test;
25+
26+
import com.cloud.storage.Storage;
27+
import com.google.common.util.concurrent.AtomicLongMap;
28+
import com.google.gson.JsonObject;
29+
import com.google.gson.JsonParser;
30+
import com.google.gson.stream.JsonReader;
31+
import com.google.gson.stream.JsonWriter;
32+
33+
/**
34+
* The adapter that turns every counter in the usage report into a JSON object.
35+
* Its handling of keys decides most of the payload's wire format.
36+
*/
37+
public class AtomicGsonAdapterTest {
38+
39+
private final AtomicGsonAdapter adapter = new AtomicGsonAdapter();
40+
41+
private String write(AtomicLongMap<Object> value) throws IOException {
42+
StringWriter out = new StringWriter();
43+
try (JsonWriter writer = new JsonWriter(out)) {
44+
writer.setSerializeNulls(true);
45+
adapter.write(writer, value);
46+
}
47+
return out.toString();
48+
}
49+
50+
@Test
51+
public void testNullMapIsWrittenAsJsonNull() throws IOException {
52+
Assert.assertEquals("null", write(null));
53+
}
54+
55+
@Test
56+
public void testEmptyMapIsWrittenAsEmptyObject() throws IOException {
57+
Assert.assertEquals("{}", write(AtomicLongMap.create()));
58+
}
59+
60+
/**
61+
* AtomicLongMap.asMap() is backed by a ConcurrentHashMap, so the order of keys
62+
* within a counter object is not deterministic. The receiving service must treat
63+
* these as unordered objects; only the key/value pairs are part of the contract.
64+
*/
65+
@Test
66+
public void testCountsAreWrittenAsNumbers() throws IOException {
67+
AtomicLongMap<Object> counter = AtomicLongMap.create();
68+
counter.getAndIncrement("KVM");
69+
counter.getAndIncrement("KVM");
70+
counter.getAndIncrement("VMware");
71+
72+
JsonObject json = JsonParser.parseString(write(counter)).getAsJsonObject();
73+
Assert.assertEquals(2, json.size());
74+
Assert.assertEquals(2, json.get("KVM").getAsLong());
75+
Assert.assertEquals(1, json.get("VMware").getAsLong());
76+
}
77+
78+
/**
79+
* Boolean keys reach the wire as the strings "true" and "false"; the report uses
80+
* these for ha_enabled, dynamically_scalable, compute_only and use_local_storage.
81+
*/
82+
@Test
83+
public void testBooleanKeysBecomeStringKeys() throws IOException {
84+
AtomicLongMap<Object> counter = AtomicLongMap.create();
85+
counter.getAndIncrement(Boolean.TRUE);
86+
counter.getAndIncrement(Boolean.FALSE);
87+
counter.getAndIncrement(Boolean.FALSE);
88+
89+
String json = write(counter);
90+
Assert.assertTrue(json, json.contains("\"true\":1"));
91+
Assert.assertTrue(json, json.contains("\"false\":2"));
92+
}
93+
94+
/**
95+
* Keys go through String.valueOf(), i.e. toString(), so an enum that overrides
96+
* toString() is serialized by that override and not by its constant name.
97+
*/
98+
@Test
99+
public void testEnumKeysUseToStringNotConstantName() throws IOException {
100+
AtomicLongMap<Object> counter = AtomicLongMap.create();
101+
counter.getAndIncrement(Storage.ProvisioningType.THIN);
102+
103+
Assert.assertEquals("{\"thin\":1}", write(counter));
104+
}
105+
106+
@Test
107+
public void testNullKeysCannotReachThePayload() throws IOException {
108+
AtomicLongMap<Object> counter = AtomicLongMap.create();
109+
counter.getAndIncrement("KVM");
110+
111+
// AtomicLongMap rejects null keys outright, so a "null" key can only ever
112+
// appear if a caller stringifies before counting. Guard the assumption.
113+
try {
114+
counter.getAndIncrement(null);
115+
Assert.fail("AtomicLongMap unexpectedly accepted a null key");
116+
} catch (NullPointerException expected) {
117+
// expected
118+
}
119+
120+
Assert.assertEquals("{\"KVM\":1}", write(counter));
121+
}
122+
123+
@Test
124+
public void testReadConsumesNullAndReturnsNull() throws IOException {
125+
try (JsonReader reader = new JsonReader(new StringReader("null"))) {
126+
Assert.assertNull(adapter.read(reader));
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)