1 /* Any copyright is dedicated to the Public Domain.
2  * http://creativecommons.org/publicdomain/zero/1.0/
3  */
4 
5 #include "TelemetryTestHelpers.h"
6 
7 #include "core/TelemetryCommon.h"
8 #include "core/TelemetryOrigin.h"
9 #include "gtest/gtest.h"
10 #include "js/Array.h"  // JS::GetArrayLength, JS::IsArrayObject
11 #include "mozilla/CycleCollectedJSContext.h"
12 #include "mozilla/Unused.h"
13 #include "nsPrintfCString.h"
14 
15 using namespace mozilla;
16 
17 // Helper methods provided to simplify writing tests and meant to be used in C++
18 // Gtests.
19 namespace TelemetryTestHelpers {
20 
CheckUintScalar(const char * aName,JSContext * aCx,JS::HandleValue aSnapshot,uint32_t expectedValue)21 void CheckUintScalar(const char* aName, JSContext* aCx,
22                      JS::HandleValue aSnapshot, uint32_t expectedValue) {
23   // Validate the value of the test scalar.
24   JS::RootedValue value(aCx);
25   JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
26   ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value))
27   << "The test scalar must be reported.";
28   JS_GetProperty(aCx, scalarObj, aName, &value);
29 
30   ASSERT_TRUE(value.isInt32())
31   << "The scalar value must be of the correct type.";
32   ASSERT_TRUE(value.toInt32() >= 0)
33   << "The uint scalar type must contain a value >= 0.";
34   ASSERT_EQ(static_cast<uint32_t>(value.toInt32()), expectedValue)
35       << "The scalar value must match the expected value.";
36 }
37 
CheckBoolScalar(const char * aName,JSContext * aCx,JS::HandleValue aSnapshot,bool expectedValue)38 void CheckBoolScalar(const char* aName, JSContext* aCx,
39                      JS::HandleValue aSnapshot, bool expectedValue) {
40   // Validate the value of the test scalar.
41   JS::RootedValue value(aCx);
42   JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
43   ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value))
44   << "The test scalar must be reported.";
45   ASSERT_TRUE(value.isBoolean())
46   << "The scalar value must be of the correct type.";
47   ASSERT_EQ(static_cast<bool>(value.toBoolean()), expectedValue)
48       << "The scalar value must match the expected value.";
49 }
50 
CheckStringScalar(const char * aName,JSContext * aCx,JS::HandleValue aSnapshot,const char * expectedValue)51 void CheckStringScalar(const char* aName, JSContext* aCx,
52                        JS::HandleValue aSnapshot, const char* expectedValue) {
53   // Validate the value of the test scalar.
54   JS::RootedValue value(aCx);
55   JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
56   ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value))
57   << "The test scalar must be reported.";
58   ASSERT_TRUE(value.isString())
59   << "The scalar value must be of the correct type.";
60 
61   bool sameString;
62   ASSERT_TRUE(
63       JS_StringEqualsAscii(aCx, value.toString(), expectedValue, &sameString))
64   << "JS String comparison failed";
65   ASSERT_TRUE(sameString)
66   << "The scalar value must match the expected string";
67 }
68 
CheckKeyedUintScalar(const char * aName,const char * aKey,JSContext * aCx,JS::HandleValue aSnapshot,uint32_t expectedValue)69 void CheckKeyedUintScalar(const char* aName, const char* aKey, JSContext* aCx,
70                           JS::HandleValue aSnapshot, uint32_t expectedValue) {
71   JS::RootedValue keyedScalar(aCx);
72   JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
73   // Get the aName keyed scalar object from the scalars snapshot.
74   ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
75   << "The keyed scalar must be reported.";
76 
77   CheckUintScalar(aKey, aCx, keyedScalar, expectedValue);
78 }
79 
CheckKeyedBoolScalar(const char * aName,const char * aKey,JSContext * aCx,JS::HandleValue aSnapshot,bool expectedValue)80 void CheckKeyedBoolScalar(const char* aName, const char* aKey, JSContext* aCx,
81                           JS::HandleValue aSnapshot, bool expectedValue) {
82   JS::RootedValue keyedScalar(aCx);
83   JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
84   // Get the aName keyed scalar object from the scalars snapshot.
85   ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
86   << "The keyed scalar must be reported.";
87 
88   CheckBoolScalar(aKey, aCx, keyedScalar, expectedValue);
89 }
90 
CheckNumberOfProperties(const char * aName,JSContext * aCx,JS::HandleValue aSnapshot,uint32_t expectedNumProperties)91 void CheckNumberOfProperties(const char* aName, JSContext* aCx,
92                              JS::HandleValue aSnapshot,
93                              uint32_t expectedNumProperties) {
94   JS::RootedValue keyedScalar(aCx);
95   JS::RootedObject scalarObj(aCx, &aSnapshot.toObject());
96   // Get the aName keyed scalar object from the scalars snapshot.
97   ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
98   << "The keyed scalar must be reported.";
99 
100   JS::RootedObject keyedScalarObj(aCx, &keyedScalar.toObject());
101   JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
102   ASSERT_TRUE(JS_Enumerate(aCx, keyedScalarObj, &ids))
103   << "We must be able to get keyed scalar members.";
104 
105   ASSERT_EQ(expectedNumProperties, ids.length())
106       << "The scalar must report the expected number of properties.";
107 }
108 
EventPresent(JSContext * aCx,const JS::RootedValue & aSnapshot,const nsACString & aCategory,const nsACString & aMethod,const nsACString & aObject)109 bool EventPresent(JSContext* aCx, const JS::RootedValue& aSnapshot,
110                   const nsACString& aCategory, const nsACString& aMethod,
111                   const nsACString& aObject) {
112   EXPECT_FALSE(aSnapshot.isNullOrUndefined())
113       << "Event snapshot must not be null/undefined.";
114   bool isArray = false;
115   EXPECT_TRUE(JS::IsArrayObject(aCx, aSnapshot, &isArray) && isArray)
116       << "The snapshot must be an array.";
117   JS::RootedObject arrayObj(aCx, &aSnapshot.toObject());
118   uint32_t arrayLength = 0;
119   EXPECT_TRUE(JS::GetArrayLength(aCx, arrayObj, &arrayLength))
120       << "Array must have a length.";
121   EXPECT_TRUE(arrayLength > 0) << "Array must have at least one element.";
122 
123   for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
124     JS::Rooted<JS::Value> element(aCx);
125     EXPECT_TRUE(JS_GetElement(aCx, arrayObj, arrayIdx, &element))
126         << "Must be able to get element.";
127     EXPECT_TRUE(JS::IsArrayObject(aCx, element, &isArray) && isArray)
128         << "Element must be an array.";
129     JS::RootedObject eventArray(aCx, &element.toObject());
130     uint32_t eventLength;
131     EXPECT_TRUE(JS::GetArrayLength(aCx, eventArray, &eventLength))
132         << "Event array must have a length.";
133     EXPECT_TRUE(eventLength >= 4)
134         << "Event array must have at least 4 elements (timestamp, category, "
135            "method, object).";
136 
137     JS::Rooted<JS::Value> str(aCx);
138     nsAutoJSString jsStr;
139     EXPECT_TRUE(JS_GetElement(aCx, eventArray, 1, &str))
140         << "Must be able to get category.";
141     EXPECT_TRUE(str.isString()) << "Category must be a string.";
142     EXPECT_TRUE(jsStr.init(aCx, str))
143         << "Category must be able to be init'd to a jsstring.";
144     if (NS_ConvertUTF16toUTF8(jsStr) != aCategory) {
145       continue;
146     }
147 
148     EXPECT_TRUE(JS_GetElement(aCx, eventArray, 2, &str))
149         << "Must be able to get method.";
150     EXPECT_TRUE(str.isString()) << "Method must be a string.";
151     EXPECT_TRUE(jsStr.init(aCx, str))
152         << "Method must be able to be init'd to a jsstring.";
153     if (NS_ConvertUTF16toUTF8(jsStr) != aMethod) {
154       continue;
155     }
156 
157     EXPECT_TRUE(JS_GetElement(aCx, eventArray, 3, &str))
158         << "Must be able to get object.";
159     EXPECT_TRUE(str.isString()) << "Object must be a string.";
160     EXPECT_TRUE(jsStr.init(aCx, str))
161         << "Object must be able to be init'd to a jsstring.";
162     if (NS_ConvertUTF16toUTF8(jsStr) != aObject) {
163       continue;
164     }
165 
166     // We found it!
167     return true;
168   }
169 
170   // We didn't find it!
171   return false;
172 }
173 
GetOriginSnapshot(JSContext * aCx,JS::MutableHandle<JS::Value> aResult,bool aClear)174 void GetOriginSnapshot(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
175                        bool aClear) {
176   nsCOMPtr<nsITelemetry> telemetry =
177       do_GetService("@mozilla.org/base/telemetry;1");
178 
179   JS::RootedValue originSnapshot(aCx);
180   nsresult rv;
181   rv = telemetry->GetOriginSnapshot(aClear, aCx, &originSnapshot);
182   ASSERT_EQ(rv, NS_OK) << "Snapshotting origin data must not fail.";
183   ASSERT_TRUE(originSnapshot.isObject())
184   << "The snapshot must be an object.";
185 
186   aResult.set(originSnapshot);
187 }
188 
189 /*
190  * Extracts the `a` and `b` strings from the prioData snapshot object
191  * of any length. Which looks like:
192  *
193  * [{
194  *   encoding: encodingName,
195  *   prio: {
196  *     a: <string>,
197  *     b: <string>,
198  *   },
199  * }, ...]
200  */
GetEncodedOriginStrings(JSContext * aCx,const nsCString & aEncoding,nsTArray<Tuple<nsCString,nsCString>> & aPrioStrings)201 void GetEncodedOriginStrings(
202     JSContext* aCx, const nsCString& aEncoding,
203     nsTArray<Tuple<nsCString, nsCString>>& aPrioStrings) {
204   JS::RootedValue snapshot(aCx);
205   nsresult rv;
206   rv = TelemetryOrigin::GetEncodedOriginSnapshot(false /* clear */, aCx,
207                                                  &snapshot);
208 
209   ASSERT_FALSE(NS_FAILED(rv));
210   ASSERT_FALSE(snapshot.isNullOrUndefined())
211   << "Encoded snapshot must not be null/undefined.";
212 
213   JS::RootedObject prioDataObj(aCx, &snapshot.toObject());
214   bool isArray = false;
215   ASSERT_TRUE(JS::IsArrayObject(aCx, prioDataObj, &isArray) && isArray)
216   << "The metric's origins must be in an array.";
217 
218   uint32_t length = 0;
219   ASSERT_TRUE(JS::GetArrayLength(aCx, prioDataObj, &length));
220   ASSERT_TRUE(length > 0)
221   << "Length of returned array must greater than 0";
222 
223   for (auto i = 0u; i < length; ++i) {
224     JS::RootedValue arrayItem(aCx);
225     ASSERT_TRUE(JS_GetElement(aCx, prioDataObj, i, &arrayItem));
226     ASSERT_TRUE(arrayItem.isObject());
227     ASSERT_FALSE(arrayItem.isNullOrUndefined());
228 
229     JS::RootedObject arrayItemObj(aCx, &arrayItem.toObject());
230 
231     JS::RootedValue encodingVal(aCx);
232     ASSERT_TRUE(JS_GetProperty(aCx, arrayItemObj, "encoding", &encodingVal));
233     ASSERT_TRUE(encodingVal.isString());
234     nsAutoJSString jsStr;
235     ASSERT_TRUE(jsStr.init(aCx, encodingVal));
236 
237     nsPrintfCString encoding(aEncoding.get(),
238                              i % TelemetryOrigin::SizeOfPrioDatasPerMetric());
239     ASSERT_TRUE(NS_ConvertUTF16toUTF8(jsStr) == encoding)
240     << "Actual 'encoding' (" << NS_ConvertUTF16toUTF8(jsStr).get()
241     << ") must match expected (" << encoding << ")";
242 
243     JS::RootedValue prioVal(aCx);
244     ASSERT_TRUE(JS_GetProperty(aCx, arrayItemObj, "prio", &prioVal));
245     ASSERT_TRUE(prioVal.isObject());
246     ASSERT_FALSE(prioVal.isNullOrUndefined());
247 
248     JS::RootedObject prioObj(aCx, &prioVal.toObject());
249 
250     JS::RootedValue aVal(aCx);
251     nsAutoJSString aStr;
252     ASSERT_TRUE(JS_GetProperty(aCx, prioObj, "a", &aVal));
253     ASSERT_TRUE(aVal.isString());
254     ASSERT_TRUE(aStr.init(aCx, aVal));
255 
256     JS::RootedValue bVal(aCx);
257     nsAutoJSString bStr;
258     ASSERT_TRUE(JS_GetProperty(aCx, prioObj, "b", &bVal));
259     ASSERT_TRUE(bVal.isString());
260     ASSERT_TRUE(bStr.init(aCx, bVal));
261 
262     aPrioStrings.AppendElement(Tuple<nsCString, nsCString>(
263         NS_ConvertUTF16toUTF8(aStr), NS_ConvertUTF16toUTF8(bStr)));
264   }
265 }
266 
GetEventSnapshot(JSContext * aCx,JS::MutableHandle<JS::Value> aResult,ProcessID aProcessType)267 void GetEventSnapshot(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
268                       ProcessID aProcessType) {
269   nsCOMPtr<nsITelemetry> telemetry =
270       do_GetService("@mozilla.org/base/telemetry;1");
271 
272   JS::RootedValue eventSnapshot(aCx);
273   nsresult rv;
274   rv = telemetry->SnapshotEvents(1 /* PRERELEASE_CHANNELS */, false /* clear */,
275                                  0 /* eventLimit */, aCx, 1 /* argc */,
276                                  &eventSnapshot);
277   ASSERT_EQ(rv, NS_OK) << "Snapshotting events must not fail.";
278   ASSERT_TRUE(eventSnapshot.isObject())
279   << "The snapshot must be an object.";
280 
281   JS::RootedValue processEvents(aCx);
282   JS::RootedObject eventObj(aCx, &eventSnapshot.toObject());
283   Unused << JS_GetProperty(aCx, eventObj,
284                            Telemetry::Common::GetNameForProcessID(aProcessType),
285                            &processEvents);
286 
287   aResult.set(processEvents);
288 }
289 
GetScalarsSnapshot(bool aKeyed,JSContext * aCx,JS::MutableHandle<JS::Value> aResult,ProcessID aProcessType)290 void GetScalarsSnapshot(bool aKeyed, JSContext* aCx,
291                         JS::MutableHandle<JS::Value> aResult,
292                         ProcessID aProcessType) {
293   nsCOMPtr<nsITelemetry> telemetry =
294       do_GetService("@mozilla.org/base/telemetry;1");
295 
296   // Get a snapshot of the scalars.
297   JS::RootedValue scalarsSnapshot(aCx);
298   nsresult rv;
299 
300   if (aKeyed) {
301     rv = telemetry->GetSnapshotForKeyedScalars(NS_LITERAL_CSTRING("main"),
302                                                false, false /* filter */, aCx,
303                                                &scalarsSnapshot);
304   } else {
305     rv = telemetry->GetSnapshotForScalars(NS_LITERAL_CSTRING("main"), false,
306                                           false /* filter */, aCx,
307                                           &scalarsSnapshot);
308   }
309 
310   // Validate the snapshot.
311   ASSERT_EQ(rv, NS_OK) << "Creating a snapshot of the data must not fail.";
312   ASSERT_TRUE(scalarsSnapshot.isObject())
313   << "The snapshot must be an object.";
314 
315   JS::RootedValue processScalars(aCx);
316   JS::RootedObject scalarObj(aCx, &scalarsSnapshot.toObject());
317   // Don't complain if no scalars for the process can be found. Just
318   // return an empty object.
319   Unused << JS_GetProperty(aCx, scalarObj,
320                            Telemetry::Common::GetNameForProcessID(aProcessType),
321                            &processScalars);
322 
323   aResult.set(processScalars);
324 }
325 
GetAndClearHistogram(JSContext * cx,nsCOMPtr<nsITelemetry> mTelemetry,const nsACString & name,bool is_keyed)326 void GetAndClearHistogram(JSContext* cx, nsCOMPtr<nsITelemetry> mTelemetry,
327                           const nsACString& name, bool is_keyed) {
328   JS::RootedValue testHistogram(cx);
329   nsresult rv =
330       is_keyed ? mTelemetry->GetKeyedHistogramById(name, cx, &testHistogram)
331                : mTelemetry->GetHistogramById(name, cx, &testHistogram);
332 
333   ASSERT_EQ(rv, NS_OK) << "Cannot fetch histogram";
334 
335   // Clear the stored value
336   JS::RootedObject testHistogramObj(cx, &testHistogram.toObject());
337   JS::RootedValue rval(cx);
338   ASSERT_TRUE(JS_CallFunctionName(cx, testHistogramObj, "clear",
339                                   JS::HandleValueArray::empty(), &rval))
340   << "Cannot clear histogram";
341 }
342 
GetProperty(JSContext * cx,const char * name,JS::HandleValue valueIn,JS::MutableHandleValue valueOut)343 void GetProperty(JSContext* cx, const char* name, JS::HandleValue valueIn,
344                  JS::MutableHandleValue valueOut) {
345   JS::RootedValue property(cx);
346   JS::RootedObject valueInObj(cx, &valueIn.toObject());
347   ASSERT_TRUE(JS_GetProperty(cx, valueInObj, name, &property))
348   << "Cannot get property '" << name << "'";
349   valueOut.set(property);
350 }
351 
GetElement(JSContext * cx,uint32_t index,JS::HandleValue valueIn,JS::MutableHandleValue valueOut)352 void GetElement(JSContext* cx, uint32_t index, JS::HandleValue valueIn,
353                 JS::MutableHandleValue valueOut) {
354   JS::RootedValue element(cx);
355   JS::RootedObject valueInObj(cx, &valueIn.toObject());
356   ASSERT_TRUE(JS_GetElement(cx, valueInObj, index, &element))
357   << "Cannot get element at index '" << index << "'";
358   valueOut.set(element);
359 }
360 
GetSnapshots(JSContext * cx,nsCOMPtr<nsITelemetry> mTelemetry,const char * name,JS::MutableHandleValue valueOut,bool is_keyed)361 void GetSnapshots(JSContext* cx, nsCOMPtr<nsITelemetry> mTelemetry,
362                   const char* name, JS::MutableHandleValue valueOut,
363                   bool is_keyed) {
364   JS::RootedValue snapshots(cx);
365   nsresult rv = is_keyed ? mTelemetry->GetSnapshotForKeyedHistograms(
366                                NS_LITERAL_CSTRING("main"), false,
367                                false /* filter */, cx, &snapshots)
368                          : mTelemetry->GetSnapshotForHistograms(
369                                NS_LITERAL_CSTRING("main"), false,
370                                false /* filter */, cx, &snapshots);
371 
372   JS::RootedValue snapshot(cx);
373   GetProperty(cx, "parent", snapshots, &snapshot);
374 
375   ASSERT_EQ(rv, NS_OK) << "Cannot call histogram snapshots";
376   valueOut.set(snapshot);
377 }
378 
379 }  // namespace TelemetryTestHelpers
380