This commit is contained in:
Ryan VanderMeulen 2013-09-23 23:08:26 -04:00
Родитель 90b6bec06a e55134bbc7
Коммит d770b2ae6a
143 изменённых файлов: 14222 добавлений и 444 удалений

Просмотреть файл

@ -36,6 +36,9 @@ const DEFAULT_EDITOR_CONFIG = {
showOverviewRuler: true
};
//For telemetry
Cu.import("resource://gre/modules/Services.jsm")
/**
* Object defining the debugger view components.
*/
@ -276,6 +279,13 @@ let DebuggerView = {
if (this._editorSource.url == aSource.url && !aFlags.force) {
return this._editorSource.promise;
}
let transportType = DebuggerController.client.localTransport
? "_LOCAL"
: "_REMOTE";
//Telemetry probe
let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS";
let histogram = Services.telemetry.getHistogramById(histogramId);
let startTime = +new Date();
let deferred = promise.defer();
@ -296,6 +306,8 @@ let DebuggerView = {
DebuggerView.Sources.selectedValue = aSource.url;
DebuggerController.Breakpoints.updateEditorBreakpoints();
histogram.add(+new Date() - startTime);
// Resolve and notify that a source file was shown.
window.emit(EVENTS.SOURCE_SHOWN, aSource);
deferred.resolve([aSource, aText]);

Просмотреть файл

@ -729,6 +729,15 @@ InspectorPanel.prototype = {
}
},
/**
* Trigger a high-priority layout change for things that need to be
* updated immediately
*/
immediateLayoutChange: function Inspector_immediateLayoutChange()
{
this.emit("layout-change");
},
/**
* Schedule a low-priority change event for things like paint
* and resize.

Просмотреть файл

@ -17,7 +17,7 @@ function test() {
}
function getInspectorProp(aName)
function getInspectorComputedProp(aName)
{
let computedview = inspector.sidebar.getWindowForTab("computedview").computedview.view;
for each (let view in computedview.propertyViews) {
@ -27,6 +27,18 @@ function test() {
}
return null;
}
function getInspectorRuleProp(aName)
{
let ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
let inlineStyles = ruleview._elementStyle.rules[0];
for each (let prop in inlineStyles.textProps) {
if (prop.name == aName) {
return prop;
}
}
return null;
}
function runInspectorTests(aInspector)
{
@ -40,50 +52,93 @@ function test() {
testDiv.style.fontSize = "10px";
// Start up the style inspector panel...
inspector.once("computed-view-refreshed", stylePanelTests);
inspector.once("computed-view-refreshed", computedStylePanelTests);
inspector.selection.setNode(testDiv);
});
}
function stylePanelTests()
function computedStylePanelTests()
{
let computedview = inspector.sidebar.getWindowForTab("computedview").computedview;
ok(computedview, "Style Panel has a cssHtmlTree");
let propView = getInspectorProp("font-size");
let propView = getInspectorComputedProp("font-size");
is(propView.value, "10px", "Style inspector should be showing the correct font size.");
inspector.once("computed-view-refreshed", stylePanelAfterChange);
inspector.once("computed-view-refreshed", computedStylePanelAfterChange);
testDiv.style.fontSize = "15px";
inspector.emit("layout-change");
testDiv.style.cssText = "font-size: 15px; color: red;";
}
function stylePanelAfterChange()
function computedStylePanelAfterChange()
{
let propView = getInspectorProp("font-size");
let propView = getInspectorComputedProp("font-size");
is(propView.value, "15px", "Style inspector should be showing the new font size.");
stylePanelNotActive();
let propView = getInspectorComputedProp("color");
is(propView.value, "#F00", "Style inspector should be showing the new color.");
computedStylePanelNotActive();
}
function stylePanelNotActive()
function computedStylePanelNotActive()
{
// Tests changes made while the style panel is not active.
inspector.sidebar.select("ruleview");
executeSoon(function() {
inspector.once("computed-view-refreshed", stylePanelAfterSwitch);
testDiv.style.fontSize = "20px";
inspector.sidebar.select("computedview");
});
testDiv.style.fontSize = "20px";
testDiv.style.color = "blue";
testDiv.style.textAlign = "center";
inspector.once("computed-view-refreshed", computedStylePanelAfterSwitch);
inspector.sidebar.select("computedview");
}
function stylePanelAfterSwitch()
function computedStylePanelAfterSwitch()
{
let propView = getInspectorProp("font-size");
is(propView.value, "20px", "Style inspector should be showing the newest font size.");
let propView = getInspectorComputedProp("font-size");
is(propView.value, "20px", "Style inspector should be showing the new font size.");
let propView = getInspectorComputedProp("color");
is(propView.value, "#00F", "Style inspector should be showing the new color.");
let propView = getInspectorComputedProp("text-align");
is(propView.value, "center", "Style inspector should be showing the new text align.");
rulePanelTests();
}
function rulePanelTests()
{
inspector.sidebar.select("ruleview");
let ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview;
ok(ruleview, "Style Panel has a ruleview");
let propView = getInspectorRuleProp("text-align");
is(propView.value, "center", "Style inspector should be showing the new text align.");
testDiv.style.textAlign = "right";
testDiv.style.color = "lightgoldenrodyellow";
testDiv.style.fontSize = "3em";
testDiv.style.textTransform = "uppercase";
inspector.once("rule-view-refreshed", rulePanelAfterChange);
}
function rulePanelAfterChange()
{
let propView = getInspectorRuleProp("text-align");
is(propView.value, "right", "Style inspector should be showing the new text align.");
let propView = getInspectorRuleProp("color");
is(propView.value, "#FAFAD2", "Style inspector should be showing the new color.")
let propView = getInspectorRuleProp("font-size");
is(propView.value, "3em", "Style inspector should be showing the new font size.");
let propView = getInspectorRuleProp("text-transform");
is(propView.value, "uppercase", "Style inspector should be showing the new text transform.");
finishTest();
}

Просмотреть файл

@ -397,6 +397,7 @@ MarkupView.prototype = {
*/
_mutationObserver: function MT__mutationObserver(aMutations)
{
let requiresLayoutChange = false;
for (let mutation of aMutations) {
let type = mutation.type;
let target = mutation.target;
@ -419,6 +420,11 @@ MarkupView.prototype = {
}
if (type === "attributes" || type === "characterData") {
container.update(false);
// Auto refresh style properties on selected node when they change.
if (type === "attributes" && container.selected) {
requiresLayoutChange = true;
}
} else if (type === "childList") {
container.childrenDirty = true;
// Update the children to take care of changes in the DOM
@ -427,6 +433,10 @@ MarkupView.prototype = {
this._updateChildren(container, {flash: true});
}
}
if (requiresLayoutChange) {
this._inspector.immediateLayoutChange();
}
this._waitForChildren().then(() => {
this._flashMutatedNodes(aMutations);
this._inspector.emit("markupmutation");

Просмотреть файл

@ -1283,13 +1283,13 @@ CssRuleView.prototype = {
{
// Ignore refreshes during editing or when no element is selected.
if (this.isEditing || !this._elementStyle) {
return promise.resolve(null);
return;
}
this._clearRules();
// Repopulate the element style.
return this._populate();
this._populate();
},
_populate: function() {

Просмотреть файл

@ -7,6 +7,7 @@ let doc;
let inspector;
let ruleView;
let testElement;
let rule;
function startTest(aInspector, aRuleView)
{
@ -43,25 +44,29 @@ function testRuleChanges()
is(selectors[2].textContent.indexOf(".testclass"), 0, "Third item is class rule.");
// Change the id and refresh.
inspector.once("rule-view-refreshed", testRuleChange1);
testElement.setAttribute("id", "differentid");
promiseDone(ruleView.nodeChanged().then(() => {
let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
is(selectors.length, 2, "Two rules visible.");
is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
is(selectors[1].textContent.indexOf(".testclass"), 0, "Second item is class rule.");
}
testElement.setAttribute("id", "testid");
return ruleView.nodeChanged();
}).then(() => {
// Put the id back.
let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
is(selectors.length, 3, "Three rules visible.");
is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
is(selectors[1].textContent.indexOf("#testid"), 0, "Second item is id rule.");
is(selectors[2].textContent.indexOf(".testclass"), 0, "Third item is class rule.");
function testRuleChange1()
{
let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
is(selectors.length, 2, "Two rules visible.");
is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
is(selectors[1].textContent.indexOf(".testclass"), 0, "Second item is class rule.");
testPropertyChanges();
}));
inspector.once("rule-view-refreshed", testRuleChange2);
testElement.setAttribute("id", "testid");
}
function testRuleChange2()
{
let selectors = ruleView.doc.querySelectorAll(".ruleview-selector");
is(selectors.length, 3, "Three rules visible.");
is(selectors[0].textContent.indexOf("element"), 0, "First item is inline style.");
is(selectors[1].textContent.indexOf("#testid"), 0, "Second item is id rule.");
is(selectors[2].textContent.indexOf(".testclass"), 0, "Third item is class rule.");
testPropertyChanges();
}
function validateTextProp(aProp, aEnabled, aName, aValue, aDesc)
@ -77,65 +82,86 @@ function validateTextProp(aProp, aEnabled, aName, aValue, aDesc)
function testPropertyChanges()
{
// Add a second margin-top value, just to make things interesting.
let rule = ruleView._elementStyle.rules[0];
rule = ruleView._elementStyle.rules[0];
let ruleEditor = ruleView._elementStyle.rules[0].editor;
inspector.once("rule-view-refreshed", testPropertyChange0);
// Add a second margin-top value, just to make things interesting.
ruleEditor.addProperty("margin-top", "5px", "");
promiseDone(expectRuleChange(rule).then(() => {
// Set the element style back to a 1px margin-top.
testElement.setAttribute("style", "margin-top: 1px; padding-top: 5px");
return ruleView.nodeChanged();
}).then(() => {
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[0], true, "margin-top", "1px", "First margin property re-enabled");
validateTextProp(rule.textProps[2], false, "margin-top", "5px", "Second margin property disabled");
}
// Now set it back to 5px, the 5px value should be re-enabled.
testElement.setAttribute("style", "margin-top: 5px; padding-top: 5px;");
return ruleView.nodeChanged();
function testPropertyChange0()
{
validateTextProp(rule.textProps[0], false, "margin-top", "1px", "Original margin property active");
}).then(() => {
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[0], false, "margin-top", "1px", "First margin property re-enabled");
validateTextProp(rule.textProps[2], true, "margin-top", "5px", "Second margin property disabled");
inspector.once("rule-view-refreshed", testPropertyChange1);
testElement.setAttribute("style", "margin-top: 1px; padding-top: 5px");
}
function testPropertyChange1()
{
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[0], true, "margin-top", "1px", "First margin property re-enabled");
validateTextProp(rule.textProps[2], false, "margin-top", "5px", "Second margin property disabled");
// Set the margin property to a value that doesn't exist in the editor.
// Should reuse the currently-enabled element (the second one.)
testElement.setAttribute("style", "margin-top: 15px; padding-top: 5px;");
return ruleView.nodeChanged();
}).then(() => {
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[0], false, "margin-top", "1px", "First margin property re-enabled");
validateTextProp(rule.textProps[2], true, "margin-top", "15px", "Second margin property disabled");
inspector.once("rule-view-refreshed", testPropertyChange2);
// Remove the padding-top attribute. Should disable the padding property but not remove it.
testElement.setAttribute("style", "margin-top: 5px;");
return ruleView.nodeChanged();
}).then(() => {
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[1], false, "padding-top", "5px", "Padding property disabled");
// Now set it back to 5px, the 5px value should be re-enabled.
testElement.setAttribute("style", "margin-top: 5px; padding-top: 5px;");
}
function testPropertyChange2()
{
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[0], false, "margin-top", "1px", "First margin property re-enabled");
validateTextProp(rule.textProps[2], true, "margin-top", "5px", "Second margin property disabled");
// Put the padding-top attribute back in, should re-enable the padding property.
testElement.setAttribute("style", "margin-top: 5px; padding-top: 25px");
return ruleView.nodeChanged();
}).then(() => {
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[1], true, "padding-top", "25px", "Padding property enabled");
inspector.once("rule-view-refreshed", testPropertyChange3);
// Add an entirely new property.
testElement.setAttribute("style", "margin-top: 5px; padding-top: 25px; padding-left: 20px;");
return ruleView.nodeChanged();
}).then(() => {
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, "Added a property");
validateTextProp(rule.textProps[3], true, "padding-left", "20px", "Padding property enabled");
// Set the margin property to a value that doesn't exist in the editor.
// Should reuse the currently-enabled element (the second one.)
testElement.setAttribute("style", "margin-top: 15px; padding-top: 5px;");
}
function testPropertyChange3()
{
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[0], false, "margin-top", "1px", "First margin property re-enabled");
validateTextProp(rule.textProps[2], true, "margin-top", "15px", "Second margin property disabled");
finishTest();
}));
inspector.once("rule-view-refreshed", testPropertyChange4);
// Remove the padding-top attribute. Should disable the padding property but not remove it.
testElement.setAttribute("style", "margin-top: 5px;");
}
function testPropertyChange4()
{
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[1], false, "padding-top", "5px", "Padding property disabled");
inspector.once("rule-view-refreshed", testPropertyChange5);
// Put the padding-top attribute back in, should re-enable the padding property.
testElement.setAttribute("style", "margin-top: 5px; padding-top: 25px");
}
function testPropertyChange5()
{
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 3, "Correct number of properties");
validateTextProp(rule.textProps[1], true, "padding-top", "25px", "Padding property enabled");
inspector.once("rule-view-refreshed", testPropertyChange6);
// Add an entirely new property
testElement.setAttribute("style", "margin-top: 5px; padding-top: 25px; padding-left: 20px;");
}
function testPropertyChange6()
{
is(rule.editor.element.querySelectorAll(".ruleview-property").length, 4, "Added a property");
validateTextProp(rule.textProps[3], true, "padding-left", "20px", "Padding property enabled");
finishTest();
}
function finishTest()
{
inspector = ruleView = null;
inspector = ruleView = rule = null;
doc = null;
gBrowser.removeCurrentTab();
finish();

Просмотреть файл

@ -224,12 +224,10 @@ function testGen() {
eventHandlers.push(variablesViewShown);
// Send the mousedown, mouseup and click events to check if the variables
// view opens.
EventUtils.sendMouseEvent({ type: "mousedown" }, messageBody, window);
EventUtils.sendMouseEvent({ type: "click" }, messageBody, window);
EventUtils.synthesizeMouse(messageBody, 2, 2, {}, HUD.iframeWindow);
if (showsVariablesView) {
info("messageBody tagName '" + messageBody.tagName + "' className '" + messageBody.className + "'");
yield undefined; // wait for the panel to open if we need to.
}

Просмотреть файл

@ -76,7 +76,7 @@
<!ENTITY help.title "App Manager">
<!ENTITY help.close "Close">
<!ENTITY help.intro "This tool will help you build and install web apps on compatible devices (i.e Firefox OS). The <strong>Apps</strong> tab will assist you in the validation and installation process of your app. The <strong>Device</strong> tab will give you information about the connected device. Use the bottom toolbar to connect to a device or start the simulator.">
<!ENTITY help.intro "This tool will help you build and install web apps on compatible devices (i.e. Firefox OS). The <strong>Apps</strong> tab will assist you in the validation and installation process of your app. The <strong>Device</strong> tab will give you information about the connected device. Use the bottom toolbar to connect to a device or start the simulator.">
<!ENTITY help.usefullLinks "Useful links:">
<!ENTITY help.appMgrDoc "Documentation: Using the App Manager">
<!ENTITY help.configuringDevice "How to setup your Firefox OS device">

Просмотреть файл

@ -92,19 +92,18 @@ function removeMockSearchDefault(aTimeoutMs) {
function test() {
waitForExplicitFinish();
Task.spawn(function(){
yield addTab("about:blank");
}).then(runTests);
runTests();
}
function setUp() {
if (!gEdit)
gEdit = document.getElementById("urlbar-edit");
yield addTab("about:blank");
yield showNavBar();
}
function tearDown() {
yield removeMockSearchDefault();
Browser.closeTab(Browser.selectedTab, { forceClose: true });
}
@ -272,15 +271,17 @@ gTests.push({
let searchSubmission = gEngine.getSubmission(search, null);
let trimmedSubmission = gEdit.trimValue(searchSubmission.uri.spec);
is(gEdit.value, trimmedSubmission, "tap search option: search conducted");
yield removeMockSearchDefault();
}
});
gTests.push({
desc: "bug 897131 - url bar update after content tap + edge swipe",
setUp: setUp,
tearDown: tearDown,
run: function testUrlbarTyping() {
let tab = yield addTab("about:mozilla");
yield showNavBar();
sendElementTap(window, gEdit);
ok(gEdit.isEditing, "focus urlbar: in editing mode");
@ -305,3 +306,62 @@ gTests.push({
}
});
gTests.push({
desc: "Bug 916383 - Invisible autocomplete items selectable by keyboard when 'your results' not shown",
tearDown: tearDown,
run: function testBug916383() {
yield addTab("about:start");
yield showNavBar();
sendElementTap(window, gEdit);
let bookmarkItem = Browser.selectedBrowser.contentWindow.BookmarksStartView._grid.querySelector("richgriditem");
// Get the first bookmark item label to make sure it will show up in 'your results'
let label = bookmarkItem.getAttribute("label");
EventUtils.sendString(label, window);
let opened = yield waitForCondition(() => gEdit.popup.popupOpen);
yield waitForCondition(() => gEdit.popup._results.itemCount > 0);
ok(!gEdit.popup._resultsContainer.hidden, "'Your results' are visible");
ok(gEdit.popup._results.itemCount > 0, "'Your results' are populated");
// Append a string to make sure it doesn't match anything in 'your results'
EventUtils.sendString("zzzzzzzzzzzzzzzzzz", window);
yield waitForCondition(() => gEdit.popup._resultsContainer.hidden);
ok(gEdit.popup._resultsContainer.hidden, "'Your results' are hidden");
ok(gEdit.popup._results.itemCount === 0, "'Your results' are empty");
EventUtils.synthesizeKey("VK_DOWN", {}, window);
is(gEdit.popup._searches.selectedIndex, 0, "key select search: first search selected");
}
});
gTests.push({
desc: "Bug 891667 - Use up arrow too",
tearDown: tearDown,
run: function testBug891667() {
yield addTab("about:start");
yield showNavBar();
sendElementTap(window, gEdit);
let bookmarkItem = Browser.selectedBrowser.contentWindow.BookmarksStartView._grid.querySelector("richgriditem");
// Get the first bookmark item label to make sure it will show up in 'your results'
let label = bookmarkItem.getAttribute("label");
EventUtils.sendString(label, window);
yield waitForCondition(() => gEdit.popup.popupOpen);
yield waitForCondition(() => gEdit.popup._results.itemCount > 0);
ok(gEdit.popup._results.itemCount > 0, "'Your results' populated");
EventUtils.synthesizeKey("VK_UP", {}, window);
is(gEdit.popup._results.selectedIndex, 0, "Pressing arrow up selects first item.");
}
});

Просмотреть файл

@ -396,11 +396,12 @@ function waitForCondition(aCondition, aTimeoutMs, aIntervalMs) {
let timeoutMs = aTimeoutMs || kDefaultWait;
let intervalMs = aIntervalMs || kDefaultInterval;
let startTime = Date.now();
let stack = new Error().stack;
function testCondition() {
let now = Date.now();
if((now - startTime) > timeoutMs) {
deferred.reject( new Error("Timed out waiting for condition to be true") );
deferred.reject( new Error("Timed out waiting for condition to be true at " + stack) );
return;
}

Просмотреть файл

@ -87,7 +87,7 @@ browser.jar:
skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
skin/classic/browser/downloads/download-summary.png (downloads/download-summary.png)
* skin/classic/browser/downloads/downloads.css (downloads/downloads.css)
* skin/classic/browser/downloads/indicator.css (downloads/indicator.css)
skin/classic/browser/downloads/indicator.css (downloads/indicator.css)
skin/classic/browser/feeds/feedIcon.png (feeds/feedIcon.png)
skin/classic/browser/feeds/feedIcon16.png (feeds/feedIcon16.png)
skin/classic/browser/feeds/audioFeedIcon.png (feeds/feedIcon.png)

Просмотреть файл

@ -0,0 +1,108 @@
.. _build_overview:
=====================
Build System Overview
=====================
This document provides an overview on how the build system works. It is
targeted at people wanting to learn about internals of the build system.
It is not meant for persons who casually interact with the build system.
That being said, knowledge empowers, so consider reading on.
The build system is composed of many different components working in
harmony to build the source tree. We begin with a graphic overview.
.. graphviz::
digraph build_components {
rankdir="LR";
"configure" -> "config.status" -> "build backend" -> "build output"
}
Phase 1: Configuration
======================
Phase 1 centers around the configure script, which is a bash shell script.
The file is generated from a file called configure.in which is written in M4
and processed using Autoconf 2.13 to create the final configure script.
You don't have to worry about how you obtain a configure file: the build system
does this for you.
The primary job of configure is to determine characteristics of the system and
compiler, apply options passed into it, and validate everything looks OK to
build. The primary output of the configure script is an executable file in the
object directory called config.status. configure also produces some additional
files (like autoconf.mk). However, the most important file in terms of
architecture is config.status.
The existence of a config.status file may be familiar to those who have worked
with Autoconf before. However, Mozilla's config.status is different from almost
any other config.status you've ever seen: it's written in Python! Instead of
having our configure script produce a shell script, we have it generating Python.
Now is as good a time as any to mention that Python is prevalent in our build
system. If we need to write code for the build system, we do it in Python.
That's just how we roll.
config.status contains 2 parts: data structures representing the output of
configure and a command-line interface for preparing/configuring/generating
an appropriate build backend. (A build backend is merely a tool used to build
the tree - like GNU Make or Tup). These data structures essentially describe
the current state of the system and what the existing build configuration looks
like. For example, it defines which compiler to use, how to invoke it, which
application features are enabled, etc. You are encouraged to open up
config.status to have a look for yourself!
Once we have emitted a config.status file, we pass into the realm of phase 2.
Phase 2: Build Backend Preparation and the Build Definition
===========================================================
Once configure has determined what the current build configuration is, we need
to apply this to the source tree so we can actually build.
What essentially happens is the automatically-produced config.status Python
script is executed as soon as configure has generated it. config.status is charged
with the task of tell a tool had to build the tree. To do this, config.status
must first scan the build system definition.
The build system definition consists of various moz.build files in the tree.
There is roughly one moz.build file per directory or pet set of related directories.
Each moz.build files defines how its part of the build config works. For example it
says I want these C++ files compiled or look for additional information in these
directories. config.status starts with the main moz.build file and then recurses
into all referenced files and directories. As the moz.build files are read, data
structures describing the overall build system definition are emitted. These data
structures are then read by a build backend generator which then converts them
into files, function calls, etc. In the case of a `make` backend, the generator
writes out Makefiles.
When config.status runs, you'll see the following output::
Reticulating splines...
Finished reading 1096 moz.build files into 1276 descriptors in 2.40s
Backend executed in 2.39s
2188 total backend files. 0 created; 1 updated; 2187 unchanged
Total wall time: 5.03s; CPU time: 3.79s; Efficiency: 75%
What this is saying is that a total of 1096 moz.build files were read. Altogether,
1276 data structures describing the build configuration were derived from them.
It took 2.40s wall time to just read these files and produce the data structures.
The 1276 data structures were fed into the build backend which then determined it
had to manage 2188 files derived from those data structures. Most of them
already existed and didn't need changed. However, 1 was updated as a result of
the new configuration. The whole process took 5.03s. Although, only 3.79s was in
CPU time. That likely means we spent roughly 25% of the time waiting on I/O.
Phase 3: Invokation of the Build Backend
========================================
When most people think of the build system, they think of phase 3. This is
where we take all the code in the tree and produce Firefox or whatever
application you are creating. Phase 3 effectively takes whatever was
generated by phase 2 and runs it. Since the dawn of Mozilla, this has been
make consuming Makefiles. However, with the transition to moz.build files,
you may soon see non-Make build backends, such as Tup or Visual Studio.
When building the tree, most of the time is spent in phase 3. This is when
header files are installed, C++ files are compiled, files are preprocessed, etc.

Просмотреть файл

@ -17,6 +17,7 @@ import mdn_theme
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.graphviz',
]
templates_path = ['_templates']

Просмотреть файл

@ -0,0 +1,44 @@
.. _environment_variables:
================================================
Environment Variables Impacting the Build System
================================================
Various environment variables have an impact on the behavior of the
build system. This document attempts to document them.
AUTOCLOBBER
If defines, the build system will automatically clobber as needed.
The default behavior is to print a message and error out when a
clobber is needed.
This variable is typically defined in a :ref:`mozconfig <mozconfig>`
file via ``mk_add_options``.
REBUILD_CHECK
If defined, the build system will print information about why
certain files were rebuilt.
This feature is disabled by default because it makes the build slower.
MACH_NO_TERMINAL_FOOTER
If defined, the terminal footer displayed when building with mach in
a TTY is disabled.
MACH_NO_WRITE_TIMES
If defined, mach commands will not prefix output lines with the
elapsed time since program start. This option is equivalent to
passing ``--log-no-times`` to mach.
MOZ_PSEUDO_DERECURSE
Activate an *experimental* build mode where make directory traversal
is derecursified. This mode should result in faster build times at
the expense of busted builds from time-to-time. The end goal is for
this build mode to be the default. At which time, this variable will
likely go away.
A value of ``1`` activates the mode with full optimizations.
A value of ``no-parallel-export`` activates the mode without
optimizations to the *export* tier, which are known to be slightly
buggy.

Просмотреть файл

@ -26,3 +26,19 @@ Glossary
generated build config and writes out files used to build the
tree. Traditionally, config.status writes out a bunch of
Makefiles.
install manifest
A file containing metadata describing file installation rules.
A large part of the build system consists of copying files
around to appropriate places. We write out special files
describing the set of required operations so we can process the
actions effeciently. These files are install manifests.
clobber build
A build performed with an initially empty object directory. All
build actions must be performed.
incremental build
A build performed with the result of a previous build in an
object directory. The build should not have to work as hard because
it will be able to reuse the work from previous builds.

Просмотреть файл

@ -15,8 +15,11 @@ Important Concepts
.. toctree::
:maxdepth: 1
build-overview
Mozconfig Files <mozconfigs>
Profile Guided Optimization <pgo>
slow
environment-variables
mozbuild
========

Просмотреть файл

@ -1,3 +1,5 @@
.. _mozconfig:
===============
mozconfig Files
===============

156
build/docs/slow.rst Normal file
Просмотреть файл

@ -0,0 +1,156 @@
.. _slow:
============================
Why the Build System is Slow
============================
A common complaint about the build system is that it's slow. There are
many reasons contributing to its slowness. We will attempt to document
them here.
First, it is important to distinguish between a :term:`clobber build`
and an :term:`incremental build`. The reasons for why each are slow can
be different.
The build does a lot of work
============================
It may not be obvious, but the main reason the build system is slow is
because it does a lot of work! The source tree consists of a few
thousand C++ files. On a modern machine, we spend over 120 minutes of CPU
core time compiling files! So, if you are looking for the root cause of
slow clobber builds, look at the sheer volume of C++ files in the tree.
You don't have enough CPU cores and MHz
=======================================
The build should be CPU bound. If the build system maintainers are
optimizing the build system perfectly, every CPU core in your machine
should be 100% saturated during a build. While this isn't currently the
case (keep reading below), generally speaking, the more CPU cores you
have in your machine and the more total MHz in your machine, the better.
**We highly recommend building with no fewer than 4 physical CPU
cores.** Please note the *physical* in this sentence. Hyperthreaded
cores (an Intel Core i7 will report 8 CPU cores but only 4 are physical
for example) only yield at most a 1.25x speedup per core.
We also recommend using the most modern CPU model possible. Haswell
chips deliver much more performance per CPU cycle than say Sandy Bridge
CPUs.
This cause impacts both clobber and incremental builds.
You are building with a slow I/O layer
======================================
The build system can be I/O bound if your I/O layer is slow. Linking
libxul on some platforms and build architectures can perform gigabytes
of I/O.
To minimize the impact of slow I/O on build performance, **we highly
recommend building with an SSD.** Power users with enough memory may opt
to build from a RAM disk. Mechanical disks should be avoided if at all
possible.
This cause impacts both clobber and incremental builds.
You don't have enough memory
============================
The build system allocates a lot of memory, especially when building
many things in parallel. If you don't have enough free system memory,
the build will cause swap activity, slowing down your system and the
build. Even if you never get to the point of swapping, the build system
performs a lot of I/O and having all accessed files in memory and the
page cache can significantly reduce the influence of the I/O layer on
the build system.
**We recommend building with no less than 8 GB of system memory.** As
always, the more memory you have, the better. For a bare bones machine
doing nothing more than building the source tree, anything more than 16
GB is likely entering the point of diminishing returns.
This cause impacts both clobber and incremental builds.
You are building with pymake
============================
Pymake is slower than GNU make. One reason is Python is generally slower
than C. The build system maintainers are consistently looking at
optimizing pymake. However, it is death by a thousand cuts.
This cause impacts both clobber and incremental builds.
You are building on Windows
===========================
Builds on Windows are slow for a few reasons. First, Windows builds use
pymake, not GNU make (because of compatibility issues with GNU make).
But, there are other sources of slowness.
New processes on Windows are about a magnitude slower to spawn than on
UNIX-y systems such as Linux. This is because Windows has optimized new
threads while the \*NIX platforms typically optimize new processes.
Anyway, the build system spawns thousands of new processes during a
build. Parts of the build that rely on rapid spawning of new processes
are slow on Windows as a result. This is most pronounced when running
*configure*. The configure file is a giant shell script and shell
scripts rely heavily on new processes. This is why configure on Windows
can run over a minute slower on Windows.
Another reason Windows builds are slower is because Windows lacks proper
symlink support. On systems that support symlinks, we can generate a
file into a staging area then symlink it into the final directory very
quickly. On Windows, we have to perform a full file copy. This incurs
much more I/O. And if done poorly, can muck with file modification
times, messing up build dependencies. As of the summer of 2013, the
impact of symlinks is being mitigated through the use
of an :term:`install manifest`.
These issues impact both clobber and incremental builds.
Recursive make traversal is slow
================================
The build system has traditionally been built by employing recursive
make. Recursive make involves make iterating through directories / make
files sequentially and executing each in turn. This is inefficient for
directories containing few targets/tasks because make could be *starved*
for work when processing these directories. Any time make is starved,
the build isn't using all available CPU cycles and the build is slower
as a result.
Work has started in bug 907365 to fix this issue by changing the way
make traverses all the make files.
The impact of slow recursive make traversal is mostly felt on
incremental builds. Traditionally, most of the wall time during a
no-op build is spent in make traversal.
make is inefficient
===================
Compared to modern build backends like Tup or Ninja, make is slow and
inefficient. We can only make make so fast. At some point, we'll hit a
performance plateau and will need to use a different tool to make builds
faster.
Please note that clobber and incremental builds are different. A clobber
build with make will likely be as fast as a clobber build with e.g. Tup.
However, Tup should vastly outperform make when it comes to incremental
builds. Therefore, this issue is mostly seen when performing incremental
builds.
C++ header dependency hell
==========================
Modifying a *.h* file can have significant impact on the build system.
If you modify a *.h* that is used by 1000 C++ files, all of those 1000
C++ files will be recompiled.
Our code base has traditionally been sloppy managing the impact of
changed headers on build performance. Bug 785103 tracks improving the
situation.
This issue mostly impacts the times of an :term:`incremental build`.

Просмотреть файл

@ -16,7 +16,6 @@
#include "nsJSPrincipals.h"
#include "nsCOMPtr.h"
#include "nsPrincipal.h"
#include "nsIContentSecurityPolicy.h"
class nsIURI;
@ -54,7 +53,6 @@ public:
virtual ~nsNullPrincipal();
nsCOMPtr<nsIURI> mURI;
nsCOMPtr<nsIContentSecurityPolicy> mCSP;
};
#endif // nsNullPrincipal_h__

Просмотреть файл

@ -149,7 +149,8 @@ nsNullPrincipal::GetHashValue(uint32_t *aResult)
NS_IMETHODIMP
nsNullPrincipal::GetSecurityPolicy(void** aSecurityPolicy)
{
// We don't actually do security policy caching.
// We don't actually do security policy caching. And it's not like anyone
// can set a security policy for us anyway.
*aSecurityPolicy = nullptr;
return NS_OK;
}
@ -157,7 +158,8 @@ nsNullPrincipal::GetSecurityPolicy(void** aSecurityPolicy)
NS_IMETHODIMP
nsNullPrincipal::SetSecurityPolicy(void* aSecurityPolicy)
{
// We don't actually do security policy caching.
// We don't actually do security policy caching. And it's not like anyone
// can set a security policy for us anyway.
return NS_OK;
}
@ -170,20 +172,16 @@ nsNullPrincipal::GetURI(nsIURI** aURI)
NS_IMETHODIMP
nsNullPrincipal::GetCsp(nsIContentSecurityPolicy** aCsp)
{
NS_IF_ADDREF(*aCsp = mCSP);
// CSP on a null principal makes no sense
*aCsp = nullptr;
return NS_OK;
}
NS_IMETHODIMP
nsNullPrincipal::SetCsp(nsIContentSecurityPolicy* aCsp)
{
// If CSP was already set, it should not be destroyed! Instead, it should
// get set anew when a new principal is created.
if (mCSP)
return NS_ERROR_ALREADY_INITIALIZED;
mCSP = aCsp;
return NS_OK;
// CSP on a null principal makes no sense
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP

Просмотреть файл

@ -49,9 +49,9 @@ function ContentSecurityPolicy() {
this._request = "";
this._requestOrigin = "";
this._weakRequestPrincipal = null;
this._requestPrincipal = "";
this._referrer = "";
this._weakDocRequest = { get : function() { return null; } };
this._docRequest = null;
CSPdebug("CSP object initialized, no policies to enforce yet");
this._cache = { };
@ -249,7 +249,7 @@ ContentSecurityPolicy.prototype = {
return;
// Save the docRequest for fetching a policy-uri
this._weakDocRequest = Cu.getWeakReference(aChannel);
this._docRequest = aChannel;
// save the document URI (minus <fragment>) and referrer for reporting
let uri = aChannel.URI.cloneIgnoringRef();
@ -260,9 +260,8 @@ ContentSecurityPolicy.prototype = {
this._requestOrigin = uri;
//store a reference to the principal, that can later be used in shouldLoad
this._weakRequestPrincipal = Cu.getWeakReference(Cc["@mozilla.org/scriptsecuritymanager;1"]
.getService(Ci.nsIScriptSecurityManager)
.getChannelPrincipal(aChannel));
this._requestPrincipal = Components.classes["@mozilla.org/scriptsecuritymanager;1"].
getService(Components.interfaces.nsIScriptSecurityManager).getChannelPrincipal(aChannel);
if (aChannel.referrer) {
let referrer = aChannel.referrer.cloneIgnoringRef();
@ -311,13 +310,13 @@ ContentSecurityPolicy.prototype = {
if (aSpecCompliant) {
newpolicy = CSPRep.fromStringSpecCompliant(aPolicy,
selfURI,
this._weakDocRequest.get(),
this._docRequest,
this,
aReportOnly);
} else {
newpolicy = CSPRep.fromString(aPolicy,
selfURI,
this._weakDocRequest.get(),
this._docRequest,
this,
aReportOnly);
}
@ -435,8 +434,8 @@ ContentSecurityPolicy.prototype = {
// we need to set an nsIChannelEventSink on the channel object
// so we can tell it to not follow redirects when posting the reports
chan.notificationCallbacks = new CSPReportRedirectSink(policy);
if (this._weakDocRequest.get()) {
chan.loadGroup = this._weakDocRequest.get().loadGroup;
if (this._docRequest) {
chan.loadGroup = this._docRequest.loadGroup;
}
chan.QueryInterface(Ci.nsIUploadChannel)
@ -455,7 +454,7 @@ ContentSecurityPolicy.prototype = {
.getService(Ci.nsIContentPolicy);
if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_CSP_REPORT,
chan.URI, this._requestOrigin,
null, null, null, this._weakRequestPrincipal.get())
null, null, null, this._requestPrincipal)
!= Ci.nsIContentPolicy.ACCEPT) {
continue; // skip unauthorized URIs
}

Просмотреть файл

@ -2681,8 +2681,7 @@ nsDocument::InitCSP(nsIChannel* aChannel)
if (csp) {
// Copy into principal
nsIPrincipal* principal = GetPrincipal();
rv = principal->SetCsp(csp);
NS_ENSURE_SUCCESS(rv, rv);
principal->SetCsp(csp);
#ifdef PR_LOGGING
PR_LOG(gCspPRLog, PR_LOG_DEBUG,
("Inserted CSP into principal %p", principal));

Просмотреть файл

@ -97,19 +97,6 @@ MOCHITEST_FILES := \
file_bug836922_npolicies.html^headers^ \
file_bug836922_npolicies_violation.sjs \
file_bug836922_npolicies_ro_violation.sjs \
test_bug886164.html \
file_bug886164.html \
file_bug886164.html^headers^ \
file_bug886164_2.html \
file_bug886164_2.html^headers^ \
file_bug886164_3.html \
file_bug886164_3.html^headers^ \
file_bug886164_4.html \
file_bug886164_4.html^headers^ \
file_bug886164_5.html \
file_bug886164_5.html^headers^ \
file_bug886164_6.html \
file_bug886164_6.html^headers^ \
test_CSP_bug916446.html \
file_CSP_bug916446.html \
file_CSP_bug916446.html^headers^ \

Просмотреть файл

@ -1,15 +0,0 @@
<html>
<head> <meta charset="utf-8"> </head>
<body>
<!-- sandbox="allow-same-origin" -->
<!-- Content-Security-Policy: default-src 'self' -->
<!-- these should be stopped by CSP -->
<img src="http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img>
<!-- these should load ok -->
<img src="/tests/content/base/test/csp/file_CSP.sjs?testid=img_good&type=img/png" />
<script src='/tests/content/base/test/csp/file_CSP.sjs?testid=scripta_bad&type=text/javascript'></script>
</body>
</html>

Просмотреть файл

@ -1 +0,0 @@
Content-Security-Policy: default-src 'self'

Просмотреть файл

@ -1,14 +0,0 @@
<html>
<head> <meta charset="utf-8"> </head>
<body>
<!-- sandbox -->
<!-- Content-Security-Policy: default-src 'self' -->
<!-- these should be stopped by CSP -->
<img src="http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img>
<!-- these should load ok -->
<img src="/tests/content/base/test/csp/file_CSP.sjs?testid=img2a_good&type=img/png" />
</body>
</html>

Просмотреть файл

@ -1 +0,0 @@
Content-Security-Policy: default-src 'self'

Просмотреть файл

@ -1,12 +0,0 @@
<html>
<head> <meta charset="utf-8"> </head>
<body>
<!-- sandbox -->
<!-- Content-Security-Policy: default-src 'none' -->
<!-- these should be stopped by CSP -->
<img src="http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=img3_bad&type=img/png"> </img>
<img src="/tests/content/base/test/csp/file_CSP.sjs?testid=img3a_bad&type=img/png" />
</body>
</html>

Просмотреть файл

@ -1 +0,0 @@
Content-Security-Policy: default-src 'none'

Просмотреть файл

@ -1,12 +0,0 @@
<html>
<head> <meta charset="utf-8"> </head>
<body>
<!-- sandbox -->
<!-- Content-Security-Policy: default-src 'none' -->
<!-- these should be stopped by CSP -->
<img src="http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=img4_bad&type=img/png"> </img>
<img src="/tests/content/base/test/csp/file_CSP.sjs?testid=img4a_bad&type=img/png" />
</body>
</html>

Просмотреть файл

@ -1 +0,0 @@
Content-Security-Policy: default-src 'none'

Просмотреть файл

@ -1,26 +0,0 @@
<!DOCTYPE HTML>
<html>
<head> <meta charset="utf-8"> </head>
<script type="text/javascript">
function ok(result, desc) {
window.parent.postMessage({ok: result, desc: desc}, "*");
}
function doStuff() {
ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
}
</script>
<script src='file_iframe_sandbox_pass.js'></script>
<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
I am sandboxed but with only inline "allow-scripts"
<!-- sandbox="allow-scripts" -->
<!-- Content-Security-Policy: default-src 'none' 'unsafe-inline'-->
<!-- these should be stopped by CSP -->
<img src="/tests/content/base/test/csp/file_CSP.sjs?testid=img5_bad&type=img/png" />
<img src="http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=img5a_bad&type=img/png"> </img>
<script src='/tests/content/base/test/csp/file_CSP.sjs?testid=script5_bad&type=text/javascript'></script>
<script src='http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=script5a_bad&type=text/javascript'></script>
</body>
</html>

Просмотреть файл

@ -1 +0,0 @@
Content-Security-Policy: default-src 'none' 'unsafe-inline';

Просмотреть файл

@ -1,35 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
</head>
<script type="text/javascript">
function ok(result, desc) {
window.parent.postMessage({ok: result, desc: desc}, "*");
}
function doStuff() {
ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
document.getElementById('a_form').submit();
// trigger the javascript: url test
sendMouseEvent({type:'click'}, 'a_link');
}
</script>
<script src='file_iframe_sandbox_pass.js'></script>
<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
I am sandboxed but with "allow-scripts"
<img src="http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=img6_bad&type=img/png"> </img>
<script src='http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=script6_bad&type=text/javascript'></script>
<form method="get" action="file_iframe_sandbox_form_fail.html" id="a_form">
First name: <input type="text" name="firstname">
Last name: <input type="text" name="lastname">
<input type="submit" onclick="doSubmit()" id="a_button">
</form>
<a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a>
</body>
</html>

Просмотреть файл

@ -1 +0,0 @@
Content-Security-Policy: default-src 'self' 'unsafe-inline';

Просмотреть файл

@ -1,185 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Bug 886164 - Enforce CSP in sandboxed iframe</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<iframe style="width:200px;height:200px;" id='cspframe' sandbox="allow-same-origin"></iframe>
<iframe style="width:200px;height:200px;" id='cspframe2' sandbox></iframe>
<iframe style="width:200px;height:200px;" id='cspframe3' sandbox="allow-same-origin"></iframe>
<iframe style="width:200px;height:200px;" id='cspframe4' sandbox></iframe>
<iframe style="width:200px;height:200px;" id='cspframe5' sandbox="allow-scripts"></iframe>
<iframe style="width:200px;height:200px;" id='cspframe6' sandbox="allow-same-origin allow-scripts"></iframe>
<script class="testbody" type="text/javascript">
var path = "/tests/content/base/test/csp/";
// These are test results: -1 means it hasn't run,
// true/false is the pass/fail result.
window.tests = {
// sandbox allow-same-origin; 'self'
img_good: -1, // same origin
img_bad: -1, //example.com
// sandbox; 'self'
img2_bad: -1, //example.com
img2a_good: -1, // same origin & is image
// sandbox allow-same-origin; 'none'
img3_bad: -1,
img3a_bad: -1,
// sandbox; 'none'
img4_bad: -1,
img4a_bad: -1,
// sandbox allow-scripts; 'none' 'unsafe-inline'
img5_bad: -1,
img5a_bad: -1,
script5_bad: -1,
script5a_bad: -1,
// sandbox allow-same-origin allow-scripts; 'self' 'unsafe-inline'
img6_bad: -1,
script6_bad: -1,
};
// a postMessage handler that is used by sandboxed iframes without
// 'allow-same-origin' to communicate pass/fail back to this main page.
// it expects to be called with an object like {ok: true/false, desc:
// <description of the test> which it then forwards to ok()
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event)
{
ok_wrapper(event.data.ok, event.data.desc);
}
var cspTestsDone = false;
var iframeSandboxTestsDone = false;
// iframe related
var completedTests = 0;
var passedTests = 0;
function ok_wrapper(result, desc) {
ok(result, desc);
completedTests++;
if (result) {
passedTests++;
}
if (completedTests === 5) {
iframeSandboxTestsDone = true;
if (cspTestsDone) {
SimpleTest.finish();
}
}
}
//csp related
// This is used to watch the blocked data bounce off CSP and allowed data
// get sent out to the wire.
function examiner() {
SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
SpecialPowers.addObserver(this, "http-on-modify-request", false);
}
examiner.prototype = {
observe: function(subject, topic, data) {
// subject should be an nsURI, and should be either allowed or blocked.
if (!SpecialPowers.can_QI(subject))
return;
var testpat = new RegExp("testid=([a-z0-9_]+)");
//_good things better be allowed!
//_bad things better be stopped!
if (topic === "http-on-modify-request") {
//these things were allowed by CSP
var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIHttpChannel"), "URI.asciiSpec");
if (!testpat.test(asciiSpec)) return;
var testid = testpat.exec(asciiSpec)[1];
window.testResult(testid,
/_good/.test(testid),
asciiSpec + " allowed by csp");
}
if(topic === "csp-on-violate-policy") {
//these were blocked... record that they were blocked
var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec");
if (!testpat.test(asciiSpec)) return;
var testid = testpat.exec(asciiSpec)[1];
window.testResult(testid,
/_bad/.test(testid),
asciiSpec + " blocked by \"" + data + "\"");
}
},
// must eventually call this to remove the listener,
// or mochitests might get borked.
remove: function() {
SpecialPowers.removeObserver(this, "csp-on-violate-policy");
SpecialPowers.removeObserver(this, "http-on-modify-request");
}
}
window.examiner = new examiner();
window.testResult = function(testname, result, msg) {
//test already complete.... forget it... remember the first result.
if (window.tests[testname] != -1)
return;
window.tests[testname] = result;
is(result, true, testname + ' test: ' + msg);
// if any test is incomplete, keep waiting
for (var v in window.tests)
if(tests[v] == -1) {
console.log(v + " is not complete");
return;
}
// ... otherwise, finish
window.examiner.remove();
cspTestsDone = true;
if (iframeSandboxTestsDone) {
SimpleTest.finish();
}
}
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv(
{'set':[["security.csp.speccompliant", true]]},
function() {
// save this for last so that our listeners are registered.
// ... this loads the testbed of good and bad requests.
document.getElementById('cspframe').src = 'file_bug886164.html';
document.getElementById('cspframe2').src = 'file_bug886164_2.html';
document.getElementById('cspframe3').src = 'file_bug886164_3.html';
document.getElementById('cspframe4').src = 'file_bug886164_4.html';
document.getElementById('cspframe5').src = 'file_bug886164_5.html';
document.getElementById('cspframe6').src = 'file_bug886164_6.html';
});
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -1933,17 +1933,15 @@ abstract public class BrowserApp extends GeckoApp
shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle());
// Clear the existing thumbnail extras so we don't share an old thumbnail.
shareIntent.removeExtra("share_screenshot");
shareIntent.removeExtra("share_screenshot_uri");
// Include the thumbnail of the page being shared.
BitmapDrawable drawable = tab.getThumbnail();
if (drawable != null) {
Bitmap thumbnail = drawable.getBitmap();
shareIntent.putExtra("share_screenshot", thumbnail);
// Kobo uses a custom intent extra for sharing thumbnails.
if (Build.MANUFACTURER.equals("Kobo")) {
if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) {
File cacheDir = getExternalCacheDir();
if (cacheDir != null) {

Просмотреть файл

@ -1501,6 +1501,10 @@ public class BrowserToolbar extends GeckoRelativeLayout
mGo.setVisibility(View.VISIBLE);
if (InputMethods.shouldDisableUrlBarUpdate(mUrlEditText.getContext())) {
return;
}
int imageResource = R.drawable.ic_url_bar_go;
String contentDescription = mActivity.getString(R.string.go);
int imeAction = EditorInfo.IME_ACTION_GO;

Просмотреть файл

@ -61,6 +61,11 @@ final class InputMethods {
return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
}
public static boolean shouldDisableUrlBarUpdate(Context context) {
String inputMethod = getCurrentInputMethod(context);
return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
}
public static boolean shouldDelayUrlBarUpdate(Context context) {
String inputMethod = getCurrentInputMethod(context);
return METHOD_SAMSUNG.equals(inputMethod) ||

Просмотреть файл

@ -0,0 +1,27 @@
#filter substitution
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="@ANDROID_BACKGROUND_TEST_PACKAGE_NAME@"
sharedUserId="@MOZ_ANDROID_SHARED_ID@"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8"
android:targetSdkVersion="14" />
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>
<uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/>
<application
android:icon="@drawable/icon"
android:label="@string/app_name" >
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:label="@string/app_name"
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@" />
</manifest>

Просмотреть файл

@ -0,0 +1,110 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.
BACKGROUND_TESTS_JAVA_FILES := \
src/announcements/TestAnnouncementsBroadcastService.java \
src/common/TestAndroidLogWriters.java \
src/common/TestBrowserContractHelpers.java \
src/common/TestDateUtils.java \
src/common/TestUtils.java \
src/common/TestWaitHelper.java \
src/db/AndroidBrowserRepositoryTestCase.java \
src/db/TestAndroidBrowserBookmarksRepository.java \
src/db/TestAndroidBrowserHistoryDataExtender.java \
src/db/TestAndroidBrowserHistoryRepository.java \
src/db/TestBookmarks.java \
src/db/TestCachedSQLiteOpenHelper.java \
src/db/TestClientsDatabase.java \
src/db/TestClientsDatabaseAccessor.java \
src/db/TestFennecTabsRepositorySession.java \
src/db/TestFennecTabsStorage.java \
src/db/TestFormHistoryRepositorySession.java \
src/db/TestPasswordsRepository.java \
src/healthreport/MockDatabaseEnvironment.java \
src/healthreport/MockHealthReportDatabaseStorage.java \
src/healthreport/MockHealthReportSQLiteOpenHelper.java \
src/healthreport/MockProfileInformationCache.java \
src/healthreport/prune/TestHealthReportPruneService.java \
src/healthreport/prune/TestPrunePolicyDatabaseStorage.java \
src/healthreport/TestEnvironmentBuilder.java \
src/healthreport/TestHealthReportBroadcastService.java \
src/healthreport/TestHealthReportDatabaseStorage.java \
src/healthreport/TestHealthReportGenerator.java \
src/healthreport/TestHealthReportProvider.java \
src/healthreport/TestHealthReportSQLiteOpenHelper.java \
src/healthreport/TestProfileInformationCache.java \
src/healthreport/upload/TestHealthReportUploadService.java \
src/helpers/AndroidSyncTestCase.java \
src/helpers/BackgroundServiceTestCase.java \
src/helpers/DBHelpers.java \
src/helpers/DBProviderTestCase.java \
src/helpers/FakeProfileTestCase.java \
src/sync/helpers/BookmarkHelpers.java \
src/sync/helpers/DefaultBeginDelegate.java \
src/sync/helpers/DefaultCleanDelegate.java \
src/sync/helpers/DefaultDelegate.java \
src/sync/helpers/DefaultFetchDelegate.java \
src/sync/helpers/DefaultFinishDelegate.java \
src/sync/helpers/DefaultGuidsSinceDelegate.java \
src/sync/helpers/DefaultSessionCreationDelegate.java \
src/sync/helpers/DefaultStoreDelegate.java \
src/sync/helpers/ExpectBeginDelegate.java \
src/sync/helpers/ExpectBeginFailDelegate.java \
src/sync/helpers/ExpectFetchDelegate.java \
src/sync/helpers/ExpectFetchSinceDelegate.java \
src/sync/helpers/ExpectFinishDelegate.java \
src/sync/helpers/ExpectFinishFailDelegate.java \
src/sync/helpers/ExpectGuidsSinceDelegate.java \
src/sync/helpers/ExpectInvalidRequestFetchDelegate.java \
src/sync/helpers/ExpectInvalidTypeStoreDelegate.java \
src/sync/helpers/ExpectManyStoredDelegate.java \
src/sync/helpers/ExpectNoGUIDsSinceDelegate.java \
src/sync/helpers/ExpectStoreCompletedDelegate.java \
src/sync/helpers/ExpectStoredDelegate.java \
src/sync/helpers/HistoryHelpers.java \
src/sync/helpers/PasswordHelpers.java \
src/sync/helpers/SessionTestHelper.java \
src/sync/helpers/SimpleSuccessBeginDelegate.java \
src/sync/helpers/SimpleSuccessCreationDelegate.java \
src/sync/helpers/SimpleSuccessFetchDelegate.java \
src/sync/helpers/SimpleSuccessFinishDelegate.java \
src/sync/helpers/SimpleSuccessStoreDelegate.java \
src/sync/TestAccountPickler.java \
src/sync/TestClientsStage.java \
src/sync/TestConfigurationMigrator.java \
src/sync/TestResetting.java \
src/sync/TestSendTabData.java \
src/sync/TestStoreTracking.java \
src/sync/TestSyncAccounts.java \
src/sync/TestSyncAuthenticatorService.java \
src/sync/TestSyncConfiguration.java \
src/sync/TestTabsRecord.java \
src/sync/TestUpgradeRequired.java \
src/sync/TestWebURLFinder.java \
src/telemetry/TestTelemetryRecorder.java \
src/testhelpers/BaseMockServerSyncStage.java \
src/testhelpers/CommandHelpers.java \
src/testhelpers/DefaultGlobalSessionCallback.java \
src/testhelpers/JPakeNumGeneratorFixed.java \
src/testhelpers/MockAbstractNonRepositorySyncStage.java \
src/testhelpers/MockClientsDatabaseAccessor.java \
src/testhelpers/MockClientsDataDelegate.java \
src/testhelpers/MockGlobalSession.java \
src/testhelpers/MockPrefsGlobalSession.java \
src/testhelpers/MockRecord.java \
src/testhelpers/MockServerSyncStage.java \
src/testhelpers/MockSharedPreferences.java \
src/testhelpers/WaitHelper.java \
src/testhelpers/WBORepository.java \
$(NULL)
BACKGROUND_TESTS_RES_FILES := \
res/drawable-hdpi/icon.png \
res/drawable-ldpi/icon.png \
res/drawable-mdpi/icon.png \
res/layout/main.xml \
res/values/strings.xml \
$(NULL)

Двоичные данные
mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.7 KiB

Двоичные данные
mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.2 KiB

Двоичные данные
mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.6 KiB

Просмотреть файл

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/app_name" />
</LinearLayout>

Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Gecko Background Tests</string>
</resources>

Просмотреть файл

@ -0,0 +1,99 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.announcements;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
public class TestAnnouncementsBroadcastService
extends BackgroundServiceTestCase<TestAnnouncementsBroadcastService.MockAnnouncementsBroadcastService> {
public static class MockAnnouncementsBroadcastService extends AnnouncementsBroadcastService {
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
protected void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
}
public TestAnnouncementsBroadcastService() {
super(MockAnnouncementsBroadcastService.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
// We can't mock AlarmManager since it has a package-private constructor, so instead we reset
// the alarm by hand.
cancelAlarm(getServiceIntent());
}
@Override
public void tearDown() throws Exception {
cancelAlarm(getServiceIntent());
AnnouncementsConstants.DISABLED = false;
super.tearDown();
}
protected Intent getServiceIntent() {
final Intent intent = new Intent(getContext(), AnnouncementsService.class);
return intent;
}
public void testIgnoredServicePrefIntents() throws Exception {
// Intent without "enabled" extra is ignored.
intent.setAction(AnnouncementsConstants.ACTION_ANNOUNCEMENTS_PREF);
startService(intent);
await();
assertFalse(isServiceAlarmSet(getServiceIntent()));
}
public void testServicePrefIntentDisabled() throws Exception {
intent.setAction(AnnouncementsConstants.ACTION_ANNOUNCEMENTS_PREF)
.putExtra("enabled", false);
startService(intent);
await();
assertFalse(isServiceAlarmSet(getServiceIntent()));
}
public void testServicePrefIntentEnabled() throws Exception {
intent.setAction(AnnouncementsConstants.ACTION_ANNOUNCEMENTS_PREF)
.putExtra("enabled", true);
startService(intent);
await();
assertTrue(isServiceAlarmSet(getServiceIntent()));
}
public void testServicePrefCancelled() throws Exception {
intent.setAction(AnnouncementsConstants.ACTION_ANNOUNCEMENTS_PREF)
.putExtra("enabled", true);
startService(intent);
await();
assertTrue(isServiceAlarmSet(getServiceIntent()));
barrier.reset();
intent.putExtra("enabled", false);
startService(intent);
await();
assertFalse(isServiceAlarmSet(getServiceIntent()));
}
}

Просмотреть файл

@ -0,0 +1,68 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.common;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter;
import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter;
import org.mozilla.gecko.background.common.log.writers.LogWriter;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
public class TestAndroidLogWriters extends AndroidSyncTestCase {
public static final String TEST_LOG_TAG = "TestAndroidLogWriters";
public static final String TEST_MESSAGE_1 = "LOG TEST MESSAGE one";
public static final String TEST_MESSAGE_2 = "LOG TEST MESSAGE two";
public static final String TEST_MESSAGE_3 = "LOG TEST MESSAGE three";
public void setUp() {
Logger.stopLoggingToAll();
}
public void tearDown() {
Logger.resetLogging();
}
/**
* Verify these *all* appear in the Android log by using
* <code>adb logcat | grep TestAndroidLogWriters</code> after executing
* <code>adb shell setprop log.tag.TestAndroidLogWriters ERROR</code>.
* <p>
* This writer does not use the Android log levels!
*/
public void testAndroidLogWriter() {
LogWriter lw = new AndroidLogWriter();
Logger.error(TEST_LOG_TAG, TEST_MESSAGE_1, new RuntimeException());
Logger.startLoggingTo(lw);
Logger.error(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.warn(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.info(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.debug(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.trace(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.stopLoggingTo(lw);
Logger.error(TEST_LOG_TAG, TEST_MESSAGE_3, new RuntimeException());
}
/**
* Verify only *some* of these appear in the Android log by using
* <code>adb logcat | grep TestAndroidLogWriters</code> after executing
* <code>adb shell setprop log.tag.TestAndroidLogWriters INFO</code>.
* <p>
* This writer should use the Android log levels!
*/
public void testAndroidLevelCachingLogWriter() throws Exception {
LogWriter lw = new AndroidLevelCachingLogWriter(new AndroidLogWriter());
Logger.error(TEST_LOG_TAG, TEST_MESSAGE_1, new RuntimeException());
Logger.startLoggingTo(lw);
Logger.error(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.warn(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.info(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.debug(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.trace(TEST_LOG_TAG, TEST_MESSAGE_2);
Logger.stopLoggingTo(lw);
Logger.error(TEST_LOG_TAG, TEST_MESSAGE_3, new RuntimeException());
}
}

Просмотреть файл

@ -0,0 +1,28 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.common;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
public class TestBrowserContractHelpers extends AndroidSyncTestCase {
public void testBookmarkCodes() {
final String[] strings = {
// Observe omissions: "microsummary", "item".
"folder", "bookmark", "separator", "livemark", "query"
};
for (int i = 0; i < strings.length; ++i) {
assertEquals(strings[i], BrowserContractHelpers.typeStringForCode(i));
assertEquals(i, BrowserContractHelpers.typeCodeForString(strings[i]));
}
assertEquals(null, BrowserContractHelpers.typeStringForCode(-1));
assertEquals(null, BrowserContractHelpers.typeStringForCode(100));
assertEquals(-1, BrowserContractHelpers.typeCodeForString(null));
assertEquals(-1, BrowserContractHelpers.typeCodeForString("folder "));
assertEquals(-1, BrowserContractHelpers.typeCodeForString("FOLDER"));
assertEquals(-1, BrowserContractHelpers.typeCodeForString(""));
assertEquals(-1, BrowserContractHelpers.typeCodeForString("nope"));
}
}

Просмотреть файл

@ -0,0 +1,83 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.common;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.TimeZone;
import junit.framework.TestCase;
import org.mozilla.gecko.background.common.DateUtils.DateFormatter;
//import android.util.SparseArray;
public class TestDateUtils extends TestCase {
// Our old, correct implementation -- used to test the new one.
public static String getDateStringUsingFormatter(long time) {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format.format(time);
}
private void checkDateString(long time) {
assertEquals(getDateStringUsingFormatter(time),
new DateUtils.DateFormatter().getDateString(time));
}
public void testDateImplementations() {
checkDateString(1L);
checkDateString(System.currentTimeMillis());
checkDateString(1379118065844L);
checkDateString(1379110000000L);
for (long i = 0L; i < (2 * GlobalConstants.MILLISECONDS_PER_DAY); i += 11000) {
checkDateString(i);
}
}
@SuppressWarnings("static-method")
public void testReuse() {
DateFormatter formatter = new DateFormatter();
long time = System.currentTimeMillis();
assertEquals(formatter.getDateString(time), formatter.getDateString(time));
}
// Perf tests. Disabled until you need them.
/*
@SuppressWarnings("static-method")
public void testDateTiming() {
long start = 1379118000000L;
long end = 1379118045844L;
long t0 = android.os.SystemClock.elapsedRealtime();
for (long i = start; i < end; ++i) {
DateUtils.getDateString(i);
}
long t1 = android.os.SystemClock.elapsedRealtime();
System.err.println("CALENDAR: " + (t1 - t0));
t0 = android.os.SystemClock.elapsedRealtime();
for (long i = start; i < end; ++i) {
getDateStringFormatter(i);
}
t1 = android.os.SystemClock.elapsedRealtime();
System.err.println("FORMATTER: " + (t1 - t0));
}
@SuppressWarnings("static-method")
public void testDayTiming() {
long start = 33 * 365;
long end = start + 90;
int reps = 1;
long t0 = android.os.SystemClock.elapsedRealtime();
for (long i = start; i < end; ++i) {
for (int j = 0; j < reps; ++j) {
DateUtils.getDateStringForDay(i);
}
}
long t1 = android.os.SystemClock.elapsedRealtime();
System.err.println("Non-memo: " + (t1 - t0));
}
*/
}

Просмотреть файл

@ -0,0 +1,80 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.common;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.Utils;
import android.os.Bundle;
public class TestUtils extends AndroidSyncTestCase {
protected static void assertStages(String[] all, String[] sync, String[] skip, String[] expected) {
final Set<String> sAll = new HashSet<String>();
for (String s : all) {
sAll.add(s);
}
List<String> sSync = null;
if (sync != null) {
sSync = new ArrayList<String>();
for (String s : sync) {
sSync.add(s);
}
}
List<String> sSkip = null;
if (skip != null) {
sSkip = new ArrayList<String>();
for (String s : skip) {
sSkip.add(s);
}
}
List<String> stages = new ArrayList<String>(Utils.getStagesToSync(sAll, sSync, sSkip));
Collections.sort(stages);
List<String> exp = new ArrayList<String>();
for (String e : expected) {
exp.add(e);
}
assertEquals(exp, stages);
}
public void testGetStagesToSync() {
final String[] all = new String[] { "other1", "other2", "skip1", "skip2", "sync1", "sync2" };
assertStages(all, null, null, all);
assertStages(all, new String[] { "sync1" }, null, new String[] { "sync1" });
assertStages(all, null, new String[] { "skip1", "skip2" }, new String[] { "other1", "other2", "sync1", "sync2" });
assertStages(all, new String[] { "sync1", "sync2" }, new String[] { "skip1", "skip2" }, new String[] { "sync1", "sync2" });
}
protected static void assertStagesFromBundle(String[] all, String[] sync, String[] skip, String[] expected) {
final Set<String> sAll = new HashSet<String>();
for (String s : all) {
sAll.add(s);
}
final Bundle bundle = new Bundle();
Utils.putStageNamesToSync(bundle, sync, skip);
Collection<String> ss = Utils.getStagesToSyncFromBundle(sAll, bundle);
List<String> stages = new ArrayList<String>(ss);
Collections.sort(stages);
List<String> exp = new ArrayList<String>();
for (String e : expected) {
exp.add(e);
}
assertEquals(exp, stages);
}
public void testGetStagesToSyncFromBundle() {
final String[] all = new String[] { "other1", "other2", "skip1", "skip2", "sync1", "sync2" };
assertStagesFromBundle(all, null, null, all);
assertStagesFromBundle(all, new String[] { "sync1" }, null, new String[] { "sync1" });
assertStagesFromBundle(all, null, new String[] { "skip1", "skip2" }, new String[] { "other1", "other2", "sync1", "sync2" });
assertStagesFromBundle(all, new String[] { "sync1", "sync2" }, new String[] { "skip1", "skip2" }, new String[] { "sync1", "sync2" });
}
}

Просмотреть файл

@ -0,0 +1,356 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.common;
import junit.framework.AssertionFailedError;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import org.mozilla.gecko.background.testhelpers.WaitHelper.InnerError;
import org.mozilla.gecko.background.testhelpers.WaitHelper.TimeoutError;
import org.mozilla.gecko.sync.ThreadPool;
public class TestWaitHelper extends AndroidSyncTestCase {
private static final String ERROR_UNIQUE_IDENTIFIER = "error unique identifier";
public static int NO_WAIT = 1; // Milliseconds.
public static int SHORT_WAIT = 100; // Milliseconds.
public static int LONG_WAIT = 3 * SHORT_WAIT;
private Object notifyMonitor = new Object();
// Guarded by notifyMonitor.
private boolean performNotifyCalled = false;
private boolean performNotifyErrorCalled = false;
private void setPerformNotifyCalled() {
synchronized (notifyMonitor) {
performNotifyCalled = true;
}
}
private void setPerformNotifyErrorCalled() {
synchronized (notifyMonitor) {
performNotifyErrorCalled = true;
}
}
private void resetNotifyCalled() {
synchronized (notifyMonitor) {
performNotifyCalled = false;
performNotifyErrorCalled = false;
}
}
private void assertBothCalled() {
synchronized (notifyMonitor) {
assertTrue(performNotifyCalled);
assertTrue(performNotifyErrorCalled);
}
}
private void assertErrorCalled() {
synchronized (notifyMonitor) {
assertFalse(performNotifyCalled);
assertTrue(performNotifyErrorCalled);
}
}
private void assertCalled() {
synchronized (notifyMonitor) {
assertTrue(performNotifyCalled);
assertFalse(performNotifyErrorCalled);
}
}
public WaitHelper waitHelper;
public TestWaitHelper() {
super();
}
public void setUp() {
WaitHelper.resetTestWaiter();
waitHelper = WaitHelper.getTestWaiter();
resetNotifyCalled();
}
public void tearDown() {
assertTrue(waitHelper.isIdle());
}
public Runnable performNothingRunnable() {
return new Runnable() {
public void run() {
}
};
}
public Runnable performNotifyRunnable() {
return new Runnable() {
public void run() {
setPerformNotifyCalled();
waitHelper.performNotify();
}
};
}
public Runnable performNotifyAfterDelayRunnable(final int delayInMillis) {
return new Runnable() {
public void run() {
try {
Thread.sleep(delayInMillis);
} catch (InterruptedException e) {
fail("Interrupted.");
}
setPerformNotifyCalled();
waitHelper.performNotify();
}
};
}
public Runnable performNotifyErrorRunnable() {
return new Runnable() {
public void run() {
setPerformNotifyCalled();
waitHelper.performNotify(new AssertionFailedError(ERROR_UNIQUE_IDENTIFIER));
}
};
}
public Runnable inThreadPool(final Runnable runnable) {
return new Runnable() {
@Override
public void run() {
ThreadPool.run(runnable);
}
};
}
public Runnable inThread(final Runnable runnable) {
return new Runnable() {
@Override
public void run() {
new Thread(runnable).start();
}
};
}
protected void expectAssertionFailedError(Runnable runnable) {
try {
waitHelper.performWait(runnable);
} catch (InnerError e) {
AssertionFailedError inner = (AssertionFailedError)e.innerError;
setPerformNotifyErrorCalled();
String message = inner.getMessage();
assertTrue("Expected '" + message + "' to contain '" + ERROR_UNIQUE_IDENTIFIER + "'",
message.contains(ERROR_UNIQUE_IDENTIFIER));
}
}
protected void expectAssertionFailedErrorAfterDelay(int wait, Runnable runnable) {
try {
waitHelper.performWait(wait, runnable);
} catch (InnerError e) {
AssertionFailedError inner = (AssertionFailedError)e.innerError;
setPerformNotifyErrorCalled();
String message = inner.getMessage();
assertTrue("Expected '" + message + "' to contain '" + ERROR_UNIQUE_IDENTIFIER + "'",
message.contains(ERROR_UNIQUE_IDENTIFIER));
}
}
public void testPerformWait() {
waitHelper.performWait(performNotifyRunnable());
assertCalled();
}
public void testPerformWaitInThread() {
waitHelper.performWait(inThread(performNotifyRunnable()));
assertCalled();
}
public void testPerformWaitInThreadPool() {
waitHelper.performWait(inThreadPool(performNotifyRunnable()));
assertCalled();
}
public void testPerformTimeoutWait() {
waitHelper.performWait(SHORT_WAIT, performNotifyRunnable());
assertCalled();
}
public void testPerformTimeoutWaitInThread() {
waitHelper.performWait(SHORT_WAIT, inThread(performNotifyRunnable()));
assertCalled();
}
public void testPerformTimeoutWaitInThreadPool() {
waitHelper.performWait(SHORT_WAIT, inThreadPool(performNotifyRunnable()));
assertCalled();
}
public void testPerformErrorWaitInThread() {
expectAssertionFailedError(inThread(performNotifyErrorRunnable()));
assertBothCalled();
}
public void testPerformErrorWaitInThreadPool() {
expectAssertionFailedError(inThreadPool(performNotifyErrorRunnable()));
assertBothCalled();
}
public void testPerformErrorTimeoutWaitInThread() {
expectAssertionFailedErrorAfterDelay(SHORT_WAIT, inThread(performNotifyErrorRunnable()));
assertBothCalled();
}
public void testPerformErrorTimeoutWaitInThreadPool() {
expectAssertionFailedErrorAfterDelay(SHORT_WAIT, inThreadPool(performNotifyErrorRunnable()));
assertBothCalled();
}
public void testTimeout() {
try {
waitHelper.performWait(SHORT_WAIT, performNothingRunnable());
} catch (TimeoutError e) {
setPerformNotifyErrorCalled();
assertEquals(SHORT_WAIT, e.waitTimeInMillis);
}
assertErrorCalled();
}
/**
* This will pass. The sequence in the main thread is:
* - A short delay.
* - performNotify is called.
* - performWait is called and immediately finds that performNotify was called before.
*/
public void testDelay() {
try {
waitHelper.performWait(1, performNotifyAfterDelayRunnable(SHORT_WAIT));
} catch (AssertionFailedError e) {
setPerformNotifyErrorCalled();
assertTrue(e.getMessage(), e.getMessage().contains("TIMEOUT"));
}
assertCalled();
}
public Runnable performNotifyMultipleTimesRunnable() {
return new Runnable() {
public void run() {
waitHelper.performNotify();
setPerformNotifyCalled();
waitHelper.performNotify();
}
};
}
public void testPerformNotifyMultipleTimesFails() {
try {
waitHelper.performWait(NO_WAIT, performNotifyMultipleTimesRunnable()); // Not run on thread, so runnable executes before performWait looks for notifications.
} catch (WaitHelper.MultipleNotificationsError e) {
setPerformNotifyErrorCalled();
}
assertBothCalled();
assertFalse(waitHelper.isIdle()); // First perform notify should be hanging around.
waitHelper.performWait(NO_WAIT, performNothingRunnable());
}
public void testNestedWaitsAndNotifies() {
waitHelper.performWait(new Runnable() {
@Override
public void run() {
waitHelper.performWait(new Runnable() {
public void run() {
setPerformNotifyCalled();
waitHelper.performNotify();
}
});
setPerformNotifyErrorCalled();
waitHelper.performNotify();
}
});
assertBothCalled();
}
public void testAssertIsReported() {
try {
waitHelper.performWait(1, new Runnable() {
@Override
public void run() {
assertTrue("unique identifier", false);
}
});
} catch (AssertionFailedError e) {
setPerformNotifyErrorCalled();
assertTrue(e.getMessage(), e.getMessage().contains("unique identifier"));
}
assertErrorCalled();
}
/**
* The inner wait will timeout, but the outer wait will succeed. The sequence in the helper thread is:
* - A short delay.
* - performNotify is called.
*
* The sequence in the main thread is:
* - performWait is called and times out because the helper thread does not call
* performNotify quickly enough.
*/
public void testDelayInThread() throws InterruptedException {
waitHelper.performWait(new Runnable() {
@Override
public void run() {
try {
waitHelper.performWait(NO_WAIT, inThread(new Runnable() {
public void run() {
try {
Thread.sleep(SHORT_WAIT);
} catch (InterruptedException e) {
fail("Interrupted.");
}
setPerformNotifyCalled();
waitHelper.performNotify();
}
}));
} catch (WaitHelper.TimeoutError e) {
setPerformNotifyErrorCalled();
assertEquals(NO_WAIT, e.waitTimeInMillis);
}
}
});
assertBothCalled();
}
/**
* The inner wait will timeout, but the outer wait will succeed. The sequence in the helper thread is:
* - A short delay.
* - performNotify is called.
*
* The sequence in the main thread is:
* - performWait is called and times out because the helper thread does not call
* performNotify quickly enough.
*/
public void testDelayInThreadPool() throws InterruptedException {
waitHelper.performWait(new Runnable() {
@Override
public void run() {
try {
waitHelper.performWait(NO_WAIT, inThreadPool(new Runnable() {
public void run() {
try {
Thread.sleep(SHORT_WAIT);
} catch (InterruptedException e) {
fail("Interrupted.");
}
setPerformNotifyCalled();
waitHelper.performNotify();
}
}));
} catch (WaitHelper.TimeoutError e) {
setPerformNotifyErrorCalled();
assertEquals(NO_WAIT, e.waitTimeInMillis);
}
}
});
assertBothCalled();
}
}

Просмотреть файл

@ -0,0 +1,818 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.sync.helpers.DefaultBeginDelegate;
import org.mozilla.gecko.background.sync.helpers.DefaultCleanDelegate;
import org.mozilla.gecko.background.sync.helpers.DefaultFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.DefaultFinishDelegate;
import org.mozilla.gecko.background.sync.helpers.DefaultSessionCreationDelegate;
import org.mozilla.gecko.background.sync.helpers.DefaultStoreDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectBeginDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectBeginFailDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFinishFailDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectInvalidRequestFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectManyStoredDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectStoreCompletedDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate;
import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentValues;
import android.content.Context;
public abstract class AndroidBrowserRepositoryTestCase extends AndroidSyncTestCase {
protected static String LOG_TAG = "BrowserRepositoryTest";
protected static void wipe(AndroidBrowserRepositoryDataAccessor helper) {
Logger.debug(LOG_TAG, "Wiping.");
try {
helper.wipe();
} catch (NullPointerException e) {
// This will be handled in begin, here we can just ignore
// the error if it actually occurs since this is just test
// code. We will throw a ProfileDatabaseException. This
// error shouldn't occur in the future, but results from
// trying to access content providers before Fennec has
// been run at least once.
Logger.error(LOG_TAG, "ProfileDatabaseException seen in wipe. Begin should fail");
fail("NullPointerException in wipe.");
}
}
@Override
public void setUp() {
AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
wipe(helper);
assertTrue(WaitHelper.getTestWaiter().isIdle());
closeDataAccessor(helper);
}
public void tearDown() {
assertTrue(WaitHelper.getTestWaiter().isIdle());
}
protected RepositorySession createSession() {
return SessionTestHelper.createSession(
getApplicationContext(),
getRepository());
}
protected RepositorySession createAndBeginSession() {
return SessionTestHelper.createAndBeginSession(
getApplicationContext(),
getRepository());
}
protected static void dispose(RepositorySession session) {
if (session != null) {
session.abort();
}
}
/**
* Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored.
*/
public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) {
return new ExpectFetchDelegate(expected);
}
/**
* Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored.
*/
public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) {
return new ExpectGuidsSinceDelegate(expected);
}
/**
* Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any).
*/
public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() {
return new ExpectGuidsSinceDelegate(new String[] {});
}
/**
* Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored.
*/
public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) {
return new ExpectFetchSinceDelegate(timestamp, expected);
}
public static Runnable storeRunnable(final RepositorySession session, final Record record, final DefaultStoreDelegate delegate) {
return new Runnable() {
@Override
public void run() {
session.setStoreDelegate(delegate);
try {
session.store(record);
session.storeDone();
} catch (NoStoreDelegateException e) {
fail("NoStoreDelegateException should not occur.");
}
}
};
}
public static Runnable storeRunnable(final RepositorySession session, final Record record) {
return storeRunnable(session, record, new ExpectStoredDelegate(record.guid));
}
public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records, final DefaultStoreDelegate delegate) {
return new Runnable() {
@Override
public void run() {
session.setStoreDelegate(delegate);
try {
for (Record record : records) {
session.store(record);
}
session.storeDone();
} catch (NoStoreDelegateException e) {
fail("NoStoreDelegateException should not occur.");
}
}
};
}
public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records) {
return storeManyRunnable(session, records, new ExpectManyStoredDelegate(records));
}
/**
* Store a record and don't expect a store callback until we're done.
*
* @param session
* @param record
* @return Runnable.
*/
public static Runnable quietStoreRunnable(final RepositorySession session, final Record record) {
return storeRunnable(session, record, new ExpectStoreCompletedDelegate());
}
public static Runnable beginRunnable(final RepositorySession session, final DefaultBeginDelegate delegate) {
return new Runnable() {
@Override
public void run() {
try {
session.begin(delegate);
} catch (InvalidSessionTransitionException e) {
performNotify(e);
}
}
};
}
public static Runnable finishRunnable(final RepositorySession session, final DefaultFinishDelegate delegate) {
return new Runnable() {
@Override
public void run() {
try {
session.finish(delegate);
} catch (InactiveSessionException e) {
performNotify(e);
}
}
};
}
public static Runnable fetchAllRunnable(final RepositorySession session, final ExpectFetchDelegate delegate) {
return new Runnable() {
@Override
public void run() {
session.fetchAll(delegate);
}
};
}
public Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) {
return fetchAllRunnable(session, preparedExpectFetchDelegate(expectedRecords));
}
public Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
return new Runnable() {
@Override
public void run() {
session.guidsSince(timestamp, preparedExpectGuidsSinceDelegate(expected));
}
};
}
public Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
return new Runnable() {
@Override
public void run() {
session.fetchSince(timestamp, preparedExpectFetchSinceDelegate(timestamp, expected));
}
};
}
public static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final DefaultFetchDelegate delegate) {
return new Runnable() {
@Override
public void run() {
try {
session.fetch(guids, delegate);
} catch (InactiveSessionException e) {
performNotify(e);
}
}
};
}
public Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) {
return fetchRunnable(session, guids, preparedExpectFetchDelegate(expected));
}
public static Runnable cleanRunnable(final Repository repository, final boolean success, final Context context, final DefaultCleanDelegate delegate) {
return new Runnable() {
@Override
public void run() {
repository.clean(success, delegate, context);
}
};
}
protected abstract Repository getRepository();
protected abstract AndroidBrowserRepositoryDataAccessor getDataAccessor();
protected static void doStore(RepositorySession session, Record[] records) {
performWait(storeManyRunnable(session, records));
}
// Tests to implement
public abstract void testFetchAll();
public abstract void testGuidsSinceReturnMultipleRecords();
public abstract void testGuidsSinceReturnNoRecords();
public abstract void testFetchSinceOneRecord();
public abstract void testFetchSinceReturnNoRecords();
public abstract void testFetchOneRecordByGuid();
public abstract void testFetchMultipleRecordsByGuids();
public abstract void testFetchNoRecordByGuid();
public abstract void testWipe();
public abstract void testStore();
public abstract void testRemoteNewerTimeStamp();
public abstract void testLocalNewerTimeStamp();
public abstract void testDeleteRemoteNewer();
public abstract void testDeleteLocalNewer();
public abstract void testDeleteRemoteLocalNonexistent();
public abstract void testStoreIdenticalExceptGuid();
public abstract void testCleanMultipleRecords();
/*
* Test abstractions
*/
protected void basicStoreTest(Record record) {
final RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, record));
}
protected void basicFetchAllTest(Record[] expected) {
Logger.debug("rnewman", "Starting testFetchAll.");
RepositorySession session = createAndBeginSession();
Logger.debug("rnewman", "Prepared.");
AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
helper.dumpDB();
performWait(storeManyRunnable(session, expected));
helper.dumpDB();
performWait(fetchAllRunnable(session, expected));
closeDataAccessor(helper);
dispose(session);
}
/*
* Tests for clean
*/
// Input: 4 records; 2 which are to be cleaned, 2 which should remain after the clean
protected void cleanMultipleRecords(Record delete0, Record delete1, Record keep0, Record keep1, Record keep2) {
RepositorySession session = createAndBeginSession();
doStore(session, new Record[] {
delete0, delete1, keep0, keep1, keep2
});
// Force two records to appear deleted.
AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
db.updateByGuid(delete0.guid, cv);
db.updateByGuid(delete1.guid, cv);
final DefaultCleanDelegate delegate = new DefaultCleanDelegate() {
public void onCleaned(Repository repo) {
performNotify();
}
};
final Runnable cleanRunnable = cleanRunnable(
getRepository(),
true,
getApplicationContext(),
delegate);
performWait(cleanRunnable);
performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(new Record[] { keep0, keep1, keep2})));
closeDataAccessor(db);
dispose(session);
}
/*
* Tests for guidsSince
*/
protected void guidsSinceReturnMultipleRecords(Record record0, Record record1) {
RepositorySession session = createAndBeginSession();
long timestamp = System.currentTimeMillis();
String[] expected = new String[2];
expected[0] = record0.guid;
expected[1] = record1.guid;
Logger.debug(getName(), "Storing two records...");
performWait(storeManyRunnable(session, new Record[] { record0, record1 }));
Logger.debug(getName(), "Getting guids since " + timestamp + "; expecting " + expected.length);
performWait(guidsSinceRunnable(session, timestamp, expected));
dispose(session);
}
protected void guidsSinceReturnNoRecords(Record record0) {
RepositorySession session = createAndBeginSession();
// Store 1 record in the past.
performWait(storeRunnable(session, record0));
String[] expected = {};
performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected));
dispose(session);
}
/*
* Tests for fetchSince
*/
protected void fetchSinceOneRecord(Record record0, Record record1) {
RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, record0));
long timestamp = System.currentTimeMillis();
Logger.debug("fetchSinceOneRecord", "Entering synchronized section. Timestamp " + timestamp);
synchronized(this) {
try {
wait(1000);
} catch (InterruptedException e) {
Logger.warn("fetchSinceOneRecord", "Interrupted.", e);
}
}
Logger.debug("fetchSinceOneRecord", "Storing.");
performWait(storeRunnable(session, record1));
Logger.debug("fetchSinceOneRecord", "Fetching record 1.");
String[] expectedOne = new String[] { record1.guid };
performWait(fetchSinceRunnable(session, timestamp + 10, expectedOne));
Logger.debug("fetchSinceOneRecord", "Fetching both, relying on inclusiveness.");
String[] expectedBoth = new String[] { record0.guid, record1.guid };
performWait(fetchSinceRunnable(session, timestamp - 3000, expectedBoth));
Logger.debug("fetchSinceOneRecord", "Done.");
dispose(session);
}
protected void fetchSinceReturnNoRecords(Record record) {
RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, record));
long timestamp = System.currentTimeMillis();
performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {}));
dispose(session);
}
protected void fetchOneRecordByGuid(Record record0, Record record1) {
RepositorySession session = createAndBeginSession();
Record[] store = new Record[] { record0, record1 };
performWait(storeManyRunnable(session, store));
String[] guids = new String[] { record0.guid };
Record[] expected = new Record[] { record0 };
performWait(fetchRunnable(session, guids, expected));
dispose(session);
}
protected void fetchMultipleRecordsByGuids(Record record0,
Record record1, Record record2) {
RepositorySession session = createAndBeginSession();
Record[] store = new Record[] { record0, record1, record2 };
performWait(storeManyRunnable(session, store));
String[] guids = new String[] { record0.guid, record2.guid };
Record[] expected = new Record[] { record0, record2 };
performWait(fetchRunnable(session, guids, expected));
dispose(session);
}
protected void fetchNoRecordByGuid(Record record) {
RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, record));
performWait(fetchRunnable(session,
new String[] { Utils.generateGuid() },
new Record[] {}));
dispose(session);
}
/*
* Test wipe
*/
protected void doWipe(final Record record0, final Record record1) {
final RepositorySession session = createAndBeginSession();
final Runnable run = new Runnable() {
@Override
public void run() {
session.wipe(new RepositorySessionWipeDelegate() {
public void onWipeSucceeded() {
performNotify();
}
public void onWipeFailed(Exception ex) {
fail("wipe should have succeeded");
performNotify();
}
@Override
public RepositorySessionWipeDelegate deferredWipeDelegate(final ExecutorService executor) {
final RepositorySessionWipeDelegate self = this;
return new RepositorySessionWipeDelegate() {
@Override
public void onWipeSucceeded() {
new Thread(new Runnable() {
@Override
public void run() {
self.onWipeSucceeded();
}}).start();
}
@Override
public void onWipeFailed(final Exception ex) {
new Thread(new Runnable() {
@Override
public void run() {
self.onWipeFailed(ex);
}}).start();
}
@Override
public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService newExecutor) {
if (newExecutor == executor) {
return this;
}
throw new IllegalArgumentException("Can't re-defer this delegate.");
}
};
}
});
}
};
// Store 2 records.
Record[] records = new Record[] { record0, record1 };
performWait(storeManyRunnable(session, records));
performWait(fetchAllRunnable(session, records));
// Wipe.
performWait(run);
dispose(session);
}
/*
* TODO adding or subtracting from lastModified timestamps does NOTHING
* since it gets overwritten when we store stuff. See other tests
* for ways to do this properly.
*/
/*
* Record being stored has newer timestamp than existing local record, local
* record has not been modified since last sync.
*/
protected void remoteNewerTimeStamp(Record local, Record remote) {
final RepositorySession session = createAndBeginSession();
// Record existing and hasn't changed since before lastSync.
// Automatically will be assigned lastModified = current time.
performWait(storeRunnable(session, local));
remote.guid = local.guid;
// Get the timestamp and make remote newer than it
ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local });
performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate));
remote.lastModified = timestampDelegate.records.get(0).lastModified + 1000;
performWait(storeRunnable(session, remote));
Record[] expected = new Record[] { remote };
ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected);
performWait(fetchAllRunnable(session, delegate));
dispose(session);
}
/*
* Local record has a newer timestamp than the record being stored. For now,
* we just take newer (local) record)
*/
protected void localNewerTimeStamp(Record local, Record remote) {
final RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, local));
remote.guid = local.guid;
// Get the timestamp and make remote older than it
ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local });
performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate));
remote.lastModified = timestampDelegate.records.get(0).lastModified - 1000;
performWait(storeRunnable(session, remote));
// Do a fetch and make sure that we get back the local record.
Record[] expected = new Record[] { local };
performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected)));
dispose(session);
}
/*
* Insert a record that is marked as deleted, remote has newer timestamp
*/
protected void deleteRemoteNewer(Record local, Record remote) {
final RepositorySession session = createAndBeginSession();
// Record existing and hasn't changed since before lastSync.
// Automatically will be assigned lastModified = current time.
performWait(storeRunnable(session, local));
// Pass the same record to store, but mark it deleted and modified
// more recently
ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local });
performWait(fetchRunnable(session, new String[] { local.guid }, timestampDelegate));
remote.lastModified = timestampDelegate.records.get(0).lastModified + 1000;
remote.deleted = true;
remote.guid = local.guid;
performWait(storeRunnable(session, remote));
performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(new Record[]{})));
dispose(session);
}
// Store two records that are identical (this has different meanings based on the
// type of record) other than their guids. The record existing locally already
// should have its guid replaced (the assumption is that the record existed locally
// and then sync was enabled and this record existed on another sync'd device).
public void storeIdenticalExceptGuid(Record record0) {
Logger.debug("storeIdenticalExceptGuid", "Started.");
final RepositorySession session = createAndBeginSession();
Logger.debug("storeIdenticalExceptGuid", "Session is " + session);
performWait(storeRunnable(session, record0));
Logger.debug("storeIdenticalExceptGuid", "Stored record0.");
DefaultFetchDelegate timestampDelegate = getTimestampDelegate(record0.guid);
performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate));
Logger.debug("storeIdenticalExceptGuid", "fetchRunnable done.");
record0.lastModified = timestampDelegate.records.get(0).lastModified + 3000;
record0.guid = Utils.generateGuid();
Logger.debug("storeIdenticalExceptGuid", "Storing modified...");
performWait(storeRunnable(session, record0));
Logger.debug("storeIdenticalExceptGuid", "Stored modified.");
Record[] expected = new Record[] { record0 };
performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected)));
Logger.debug("storeIdenticalExceptGuid", "Fetched all. Returning.");
dispose(session);
}
// Special delegate so that we don't verify parenting is correct since
// at some points it won't be since parent folder hasn't been stored.
private DefaultFetchDelegate getTimestampDelegate(final String guid) {
return new DefaultFetchDelegate() {
@Override
public void onFetchCompleted(final long fetchEnd) {
assertEquals(guid, this.records.get(0).guid);
performNotify();
}
};
}
/*
* Insert a record that is marked as deleted, local has newer timestamp
* and was not marked deleted (so keep it)
*/
protected void deleteLocalNewer(Record local, Record remote) {
Logger.debug("deleteLocalNewer", "Begin.");
final RepositorySession session = createAndBeginSession();
Logger.debug("deleteLocalNewer", "Storing local...");
performWait(storeRunnable(session, local));
// Create an older version of a record with the same GUID.
remote.guid = local.guid;
Logger.debug("deleteLocalNewer", "Fetching...");
// Get the timestamp and make remote older than it
Record[] expected = new Record[] { local };
ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(expected);
performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate));
Logger.debug("deleteLocalNewer", "Fetched.");
remote.lastModified = timestampDelegate.records.get(0).lastModified - 1000;
Logger.debug("deleteLocalNewer", "Last modified is " + remote.lastModified);
remote.deleted = true;
Logger.debug("deleteLocalNewer", "Storing deleted...");
performWait(quietStoreRunnable(session, remote)); // This appears to do a lot of work...?!
// Do a fetch and make sure that we get back the first (local) record.
performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected)));
Logger.debug("deleteLocalNewer", "Fetched and done!");
dispose(session);
}
/*
* Insert a record that is marked as deleted, record never existed locally
*/
protected void deleteRemoteLocalNonexistent(Record remote) {
final RepositorySession session = createAndBeginSession();
long timestamp = 1000000000;
// Pass a record marked deleted to store, doesn't exist locally
remote.lastModified = timestamp;
remote.deleted = true;
performWait(quietStoreRunnable(session, remote));
ExpectFetchDelegate delegate = preparedExpectFetchDelegate(new Record[]{});
performWait(fetchAllRunnable(session, delegate));
dispose(session);
}
/*
* Tests that don't require specific records based on type of repository.
* These tests don't need to be overriden in subclasses, they will just work.
*/
public void testCreateSessionNullContext() {
Logger.debug(LOG_TAG, "In testCreateSessionNullContext.");
Repository repo = getRepository();
try {
repo.createSession(new DefaultSessionCreationDelegate(), null);
fail("Should throw.");
} catch (Exception ex) {
assertNotNull(ex);
}
}
public void testStoreNullRecord() {
final RepositorySession session = createAndBeginSession();
try {
session.setStoreDelegate(new DefaultStoreDelegate());
session.store(null);
fail("Should throw.");
} catch (Exception ex) {
assertNotNull(ex);
}
dispose(session);
}
public void testFetchNoGuids() {
final RepositorySession session = createAndBeginSession();
performWait(fetchRunnable(session, new String[] {}, new ExpectInvalidRequestFetchDelegate()));
dispose(session);
}
public void testFetchNullGuids() {
final RepositorySession session = createAndBeginSession();
performWait(fetchRunnable(session, null, new ExpectInvalidRequestFetchDelegate()));
dispose(session);
}
public void testBeginOnNewSession() {
final RepositorySession session = createSession();
performWait(beginRunnable(session, new ExpectBeginDelegate()));
dispose(session);
}
public void testBeginOnRunningSession() {
final RepositorySession session = createAndBeginSession();
try {
session.begin(new ExpectBeginFailDelegate());
} catch (InvalidSessionTransitionException e) {
dispose(session);
return;
}
fail("Should have caught InvalidSessionTransitionException.");
}
public void testBeginOnFinishedSession() throws InactiveSessionException {
final RepositorySession session = createAndBeginSession();
performWait(finishRunnable(session, new ExpectFinishDelegate()));
try {
session.begin(new ExpectBeginFailDelegate());
} catch (InvalidSessionTransitionException e) {
Logger.debug(getName(), "Yay! Got an exception.", e);
dispose(session);
return;
} catch (Exception e) {
Logger.debug(getName(), "Yay! Got an exception.", e);
dispose(session);
return;
}
fail("Should have caught InvalidSessionTransitionException.");
}
public void testFinishOnFinishedSession() throws InactiveSessionException {
final RepositorySession session = createAndBeginSession();
performWait(finishRunnable(session, new ExpectFinishDelegate()));
try {
session.finish(new ExpectFinishFailDelegate());
} catch (InactiveSessionException e) {
dispose(session);
return;
}
fail("Should have caught InactiveSessionException.");
}
public void testFetchOnInactiveSession() throws InactiveSessionException {
final RepositorySession session = createSession();
try {
session.fetch(new String[] { Utils.generateGuid() }, new DefaultFetchDelegate());
} catch (InactiveSessionException e) {
// Yay.
dispose(session);
return;
};
fail("Should have caught InactiveSessionException.");
}
public void testFetchOnFinishedSession() {
final RepositorySession session = createAndBeginSession();
Logger.debug(getName(), "Finishing...");
performWait(finishRunnable(session, new ExpectFinishDelegate()));
try {
session.fetch(new String[] { Utils.generateGuid() }, new DefaultFetchDelegate());
} catch (InactiveSessionException e) {
// Yay.
dispose(session);
return;
};
fail("Should have caught InactiveSessionException.");
}
public void testGuidsSinceOnUnstartedSession() {
final RepositorySession session = createSession();
Runnable run = new Runnable() {
@Override
public void run() {
session.guidsSince(System.currentTimeMillis(),
new RepositorySessionGuidsSinceDelegate() {
public void onGuidsSinceSucceeded(String[] guids) {
fail("Session inactive, should fail");
performNotify();
}
public void onGuidsSinceFailed(Exception ex) {
verifyInactiveException(ex);
performNotify();
}
});
}
};
performWait(run);
dispose(session);
}
private static void verifyInactiveException(Exception ex) {
if (!(ex instanceof InactiveSessionException)) {
fail("Wrong exception type");
}
}
protected void closeDataAccessor(AndroidBrowserRepositoryDataAccessor dataAccessor) {
}
}

Просмотреть файл

@ -0,0 +1,636 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.ArrayList;
import org.json.simple.JSONArray;
import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectInvalidTypeStoreDelegate;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
public class TestAndroidBrowserBookmarksRepository extends AndroidBrowserRepositoryTestCase {
@Override
protected AndroidBrowserRepository getRepository() {
/**
* Override this chain in order to avoid our test code having to create two
* sessions all the time.
*/
return new AndroidBrowserBookmarksRepository() {
@Override
protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
AndroidBrowserBookmarksRepositorySession session;
session = new AndroidBrowserBookmarksRepositorySession(this, context) {
@Override
protected synchronized void trackGUID(String guid) {
System.out.println("Ignoring trackGUID call: this is a test!");
}
};
delegate.deferredCreationDelegate().onSessionCreated(session);
}
};
}
@Override
protected AndroidBrowserRepositoryDataAccessor getDataAccessor() {
return new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
}
/**
* Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored.
*/
@Override
public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) {
ExpectFetchDelegate delegate = new ExpectFetchDelegate(expected);
delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet());
return delegate;
}
/**
* Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any).
*/
public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() {
ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet().toArray(new String[] {}));
return delegate;
}
/**
* Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored.
*/
@Override
public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) {
ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(expected);
delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet());
return delegate;
}
/**
* Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored.
*/
public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) {
ExpectFetchSinceDelegate delegate = new ExpectFetchSinceDelegate(timestamp, expected);
delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet());
return delegate;
}
// NOTE NOTE NOTE
// Must store folder before records if we we are checking that the
// records returned are the same as those sent in. If the folder isn't stored
// first, the returned records won't be identical to those stored because we
// aren't able to find the parent name/guid when we do a fetch. If you don't want
// to store a folder first, store your record in "mobile" or one of the folders
// that always exists.
public void testFetchOneWithChildren() {
BookmarkRecord folder = BookmarkHelpers.createFolder1();
BookmarkRecord bookmark1 = BookmarkHelpers.createBookmark1();
BookmarkRecord bookmark2 = BookmarkHelpers.createBookmark2();
RepositorySession session = createAndBeginSession();
Record[] records = new Record[] { folder, bookmark1, bookmark2 };
performWait(storeManyRunnable(session, records));
AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
helper.dumpDB();
closeDataAccessor(helper);
String[] guids = new String[] { folder.guid };
Record[] expected = new Record[] { folder };
performWait(fetchRunnable(session, guids, expected));
dispose(session);
}
@Override
public void testFetchAll() {
Record[] expected = new Record[3];
expected[0] = BookmarkHelpers.createFolder1();
expected[1] = BookmarkHelpers.createBookmark1();
expected[2] = BookmarkHelpers.createBookmark2();
basicFetchAllTest(expected);
}
@Override
public void testGuidsSinceReturnMultipleRecords() {
BookmarkRecord record0 = BookmarkHelpers.createBookmark1();
BookmarkRecord record1 = BookmarkHelpers.createBookmark2();
guidsSinceReturnMultipleRecords(record0, record1);
}
@Override
public void testGuidsSinceReturnNoRecords() {
guidsSinceReturnNoRecords(BookmarkHelpers.createBookmarkInMobileFolder1());
}
@Override
public void testFetchSinceOneRecord() {
fetchSinceOneRecord(BookmarkHelpers.createBookmarkInMobileFolder1(),
BookmarkHelpers.createBookmarkInMobileFolder2());
}
@Override
public void testFetchSinceReturnNoRecords() {
fetchSinceReturnNoRecords(BookmarkHelpers.createBookmark1());
}
@Override
public void testFetchOneRecordByGuid() {
fetchOneRecordByGuid(BookmarkHelpers.createBookmarkInMobileFolder1(),
BookmarkHelpers.createBookmarkInMobileFolder2());
}
@Override
public void testFetchMultipleRecordsByGuids() {
BookmarkRecord record0 = BookmarkHelpers.createFolder1();
BookmarkRecord record1 = BookmarkHelpers.createBookmark1();
BookmarkRecord record2 = BookmarkHelpers.createBookmark2();
fetchMultipleRecordsByGuids(record0, record1, record2);
}
@Override
public void testFetchNoRecordByGuid() {
fetchNoRecordByGuid(BookmarkHelpers.createBookmark1());
}
@Override
public void testWipe() {
doWipe(BookmarkHelpers.createBookmarkInMobileFolder1(),
BookmarkHelpers.createBookmarkInMobileFolder2());
}
@Override
public void testStore() {
basicStoreTest(BookmarkHelpers.createBookmark1());
}
public void testStoreFolder() {
basicStoreTest(BookmarkHelpers.createFolder1());
}
/**
* TODO: 2011-12-24, tests disabled because we no longer fail
* a store call if we get an unknown record type.
*/
/*
* Test storing each different type of Bookmark record.
* We expect any records with type other than "bookmark"
* or "folder" to fail. For now we throw these away.
*/
/*
public void testStoreMicrosummary() {
basicStoreFailTest(BookmarkHelpers.createMicrosummary());
}
public void testStoreQuery() {
basicStoreFailTest(BookmarkHelpers.createQuery());
}
public void testStoreLivemark() {
basicStoreFailTest(BookmarkHelpers.createLivemark());
}
public void testStoreSeparator() {
basicStoreFailTest(BookmarkHelpers.createSeparator());
}
*/
protected void basicStoreFailTest(Record record) {
final RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, record, new ExpectInvalidTypeStoreDelegate()));
dispose(session);
}
/*
* Re-parenting tests
*/
// Insert two records missing parent, then insert their parent.
// Make sure they end up with the correct parent on fetch.
public void testBasicReparenting() throws InactiveSessionException {
Record[] expected = new Record[] {
BookmarkHelpers.createBookmark1(),
BookmarkHelpers.createBookmark2(),
BookmarkHelpers.createFolder1()
};
doMultipleFolderReparentingTest(expected);
}
// Insert 3 folders and 4 bookmarks in different orders
// and make sure they come out parented correctly
public void testMultipleFolderReparenting1() throws InactiveSessionException {
Record[] expected = new Record[] {
BookmarkHelpers.createBookmark1(),
BookmarkHelpers.createBookmark2(),
BookmarkHelpers.createBookmark3(),
BookmarkHelpers.createFolder1(),
BookmarkHelpers.createBookmark4(),
BookmarkHelpers.createFolder3(),
BookmarkHelpers.createFolder2(),
};
doMultipleFolderReparentingTest(expected);
}
public void testMultipleFolderReparenting2() throws InactiveSessionException {
Record[] expected = new Record[] {
BookmarkHelpers.createBookmark1(),
BookmarkHelpers.createBookmark2(),
BookmarkHelpers.createBookmark3(),
BookmarkHelpers.createFolder1(),
BookmarkHelpers.createBookmark4(),
BookmarkHelpers.createFolder3(),
BookmarkHelpers.createFolder2(),
};
doMultipleFolderReparentingTest(expected);
}
public void testMultipleFolderReparenting3() throws InactiveSessionException {
Record[] expected = new Record[] {
BookmarkHelpers.createBookmark1(),
BookmarkHelpers.createBookmark2(),
BookmarkHelpers.createBookmark3(),
BookmarkHelpers.createFolder1(),
BookmarkHelpers.createBookmark4(),
BookmarkHelpers.createFolder3(),
BookmarkHelpers.createFolder2(),
};
doMultipleFolderReparentingTest(expected);
}
private void doMultipleFolderReparentingTest(Record[] expected) throws InactiveSessionException {
final RepositorySession session = createAndBeginSession();
doStore(session, expected);
ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected);
performWait(fetchAllRunnable(session, delegate));
performWait(finishRunnable(session, new ExpectFinishDelegate()));
}
/*
* Test storing identical records with different guids.
* For bookmarks identical is defined by the following fields
* being the same: title, uri, type, parentName
*/
@Override
public void testStoreIdenticalExceptGuid() {
storeIdenticalExceptGuid(BookmarkHelpers.createBookmarkInMobileFolder1());
}
/*
* More complicated situation in which we insert a folder
* followed by a couple of its children. We then insert
* the folder again but with a different guid. Children
* must still get correct parent when they are fetched.
* Store a record after with the new guid as the parent
* and make sure it works as well.
*/
public void testStoreIdenticalFoldersWithChildren() {
final RepositorySession session = createAndBeginSession();
Record record0 = BookmarkHelpers.createFolder1();
// Get timestamp so that the conflicting folder that we store below is newer.
// Children won't come back on this fetch since they haven't been stored, so remove them
// before our delegate throws a failure.
BookmarkRecord rec0 = (BookmarkRecord) record0;
rec0.children = new JSONArray();
performWait(storeRunnable(session, record0));
ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { rec0 });
performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate));
AndroidBrowserRepositoryDataAccessor helper = getDataAccessor();
helper.dumpDB();
closeDataAccessor(helper);
Record record1 = BookmarkHelpers.createBookmark1();
Record record2 = BookmarkHelpers.createBookmark2();
Record record3 = BookmarkHelpers.createFolder1();
BookmarkRecord bmk3 = (BookmarkRecord) record3;
record3.guid = Utils.generateGuid();
record3.lastModified = timestampDelegate.records.get(0).lastModified + 3000;
assert(!record0.guid.equals(record3.guid));
// Store an additional record after inserting the duplicate folder
// with new GUID. Make sure it comes back as well.
Record record4 = BookmarkHelpers.createBookmark3();
BookmarkRecord bmk4 = (BookmarkRecord) record4;
bmk4.parentID = bmk3.guid;
bmk4.parentName = bmk3.parentName;
doStore(session, new Record[] {
record1, record2, record3, bmk4
});
BookmarkRecord bmk1 = (BookmarkRecord) record1;
bmk1.parentID = record3.guid;
BookmarkRecord bmk2 = (BookmarkRecord) record2;
bmk2.parentID = record3.guid;
Record[] expect = new Record[] {
bmk1, bmk2, record3
};
fetchAllRunnable(session, preparedExpectFetchDelegate(expect));
dispose(session);
}
@Override
public void testRemoteNewerTimeStamp() {
BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
remoteNewerTimeStamp(local, remote);
}
@Override
public void testLocalNewerTimeStamp() {
BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
localNewerTimeStamp(local, remote);
}
@Override
public void testDeleteRemoteNewer() {
BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
deleteRemoteNewer(local, remote);
}
@Override
public void testDeleteLocalNewer() {
BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1();
BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2();
deleteLocalNewer(local, remote);
}
@Override
public void testDeleteRemoteLocalNonexistent() {
BookmarkRecord remote = BookmarkHelpers.createBookmark2();
deleteRemoteLocalNonexistent(remote);
}
@Override
public void testCleanMultipleRecords() {
cleanMultipleRecords(
BookmarkHelpers.createBookmarkInMobileFolder1(),
BookmarkHelpers.createBookmarkInMobileFolder2(),
BookmarkHelpers.createBookmark1(),
BookmarkHelpers.createBookmark2(),
BookmarkHelpers.createFolder1());
}
public void testBasicPositioning() {
final RepositorySession session = createAndBeginSession();
Record[] expected = new Record[] {
BookmarkHelpers.createBookmark1(),
BookmarkHelpers.createFolder1(),
BookmarkHelpers.createBookmark2()
};
System.out.println("TEST: Inserting " + expected[0].guid + ", "
+ expected[1].guid + ", "
+ expected[2].guid);
doStore(session, expected);
ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected);
performWait(fetchAllRunnable(session, delegate));
int found = 0;
boolean foundFolder = false;
for (int i = 0; i < delegate.records.size(); i++) {
BookmarkRecord rec = (BookmarkRecord) delegate.records.get(i);
if (rec.guid.equals(expected[0].guid)) {
assertEquals(0, ((BookmarkRecord) delegate.records.get(i)).androidPosition);
found++;
} else if (rec.guid.equals(expected[2].guid)) {
assertEquals(1, ((BookmarkRecord) delegate.records.get(i)).androidPosition);
found++;
} else if (rec.guid.equals(expected[1].guid)) {
foundFolder = true;
} else {
System.out.println("TEST: found " + rec.guid);
}
}
assertTrue(foundFolder);
assertEquals(2, found);
dispose(session);
}
public void testSqlInjectPurgeDeleteAndUpdateByGuid() {
// Some setup.
RepositorySession session = createAndBeginSession();
AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
// Create and insert 2 bookmarks, 2nd one is evil (attempts injection).
BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1();
BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2();
bmk2.guid = "' or '1'='1";
db.insert(bmk1);
db.insert(bmk2);
// Test 1 - updateByGuid() handles evil bookmarks correctly.
db.updateByGuid(bmk2.guid, cv);
// Query bookmarks table.
Cursor cur = getAllBookmarks();
int numBookmarks = cur.getCount();
// Ensure only the evil bookmark is marked for deletion.
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
if (guid.equals(bmk2.guid)) {
assertTrue(deleted);
} else {
assertFalse(deleted);
}
cur.moveToNext();
}
} finally {
cur.close();
}
// Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record.
try {
db.purgeDeleted();
} catch (NullCursorException e) {
e.printStackTrace();
}
cur = getAllBookmarks();
int numBookmarksAfterDeletion = cur.getCount();
// Ensure we have only 1 deleted row.
assertEquals(numBookmarksAfterDeletion, numBookmarks - 1);
// Ensure only the evil bookmark is deleted.
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
if (guid.equals(bmk2.guid)) {
fail("Evil guid was not deleted!");
} else {
assertFalse(deleted);
}
cur.moveToNext();
}
} finally {
cur.close();
}
dispose(session);
}
protected Cursor getAllBookmarks() {
Context context = getApplicationContext();
Cursor cur = context.getContentResolver().query(BrowserContractHelpers.BOOKMARKS_CONTENT_URI,
BrowserContractHelpers.BookmarkColumns, null, null, null);
return cur;
}
public void testSqlInjectFetch() {
// Some setup.
RepositorySession session = createAndBeginSession();
AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
// Create and insert 4 bookmarks, last one is evil (attempts injection).
BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1();
BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2();
BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3();
BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4();
bmk4.guid = "' or '1'='1";
db.insert(bmk1);
db.insert(bmk2);
db.insert(bmk3);
db.insert(bmk4);
// Perform a fetch.
Cursor cur = null;
try {
cur = db.fetch(new String[] { bmk3.guid, bmk4.guid });
} catch (NullCursorException e1) {
e1.printStackTrace();
}
// Ensure the correct number (2) of records were fetched and with the correct guids.
if (cur == null) {
fail("No records were fetched.");
}
try {
if (cur.getCount() != 2) {
fail("Wrong number of guids fetched!");
}
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
if (!guid.equals(bmk3.guid) && !guid.equals(bmk4.guid)) {
fail("Wrong guids were fetched!");
}
cur.moveToNext();
}
} finally {
cur.close();
}
dispose(session);
}
public void testSqlInjectDelete() {
// Some setup.
RepositorySession session = createAndBeginSession();
AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
// Create and insert 2 bookmarks, 2nd one is evil (attempts injection).
BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1();
BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2();
bmk2.guid = "' or '1'='1";
db.insert(bmk1);
db.insert(bmk2);
// Note size of table before delete.
Cursor cur = getAllBookmarks();
int numBookmarks = cur.getCount();
db.purgeGuid(bmk2.guid);
// Note size of table after delete.
cur = getAllBookmarks();
int numBookmarksAfterDelete = cur.getCount();
// Ensure size of table after delete is *only* 1 less.
assertEquals(numBookmarksAfterDelete, numBookmarks - 1);
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
if (guid.equals(bmk2.guid)) {
fail("Guid was not deleted!");
}
cur.moveToNext();
}
} finally {
cur.close();
}
dispose(session);
}
/**
* Verify that data accessor's bulkInsert actually inserts.
* @throws NullCursorException
*/
public void testBulkInsert() throws NullCursorException {
RepositorySession session = createAndBeginSession();
AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
// Have to set androidID of parent manually.
Cursor cur = db.fetch(new String[] { "mobile" } );
assertEquals(1, cur.getCount());
cur.moveToFirst();
int mobileAndroidID = RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks._ID);
BookmarkRecord bookmark1 = BookmarkHelpers.createBookmarkInMobileFolder1();
BookmarkRecord bookmark2 = BookmarkHelpers.createBookmarkInMobileFolder2();
bookmark1.androidParentID = mobileAndroidID;
bookmark2.androidParentID = mobileAndroidID;
ArrayList<Record> recordList = new ArrayList<Record>();
recordList.add(bookmark1);
recordList.add(bookmark2);
db.bulkInsert(recordList);
String[] guids = new String[] { bookmark1.guid, bookmark2.guid };
Record[] expected = new Record[] { bookmark1, bookmark2 };
performWait(fetchRunnable(session, guids, expected));
dispose(session);
}
}

Просмотреть файл

@ -0,0 +1,149 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.io.IOException;
import java.util.ArrayList;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonArrayJSONException;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataExtender;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
import android.database.Cursor;
public class TestAndroidBrowserHistoryDataExtender extends AndroidSyncTestCase {
protected AndroidBrowserHistoryDataExtender extender;
protected static final String LOG_TAG = "SyncHistoryVisitsTest";
public void setUp() {
extender = new AndroidBrowserHistoryDataExtender(getApplicationContext());
extender.wipe();
}
public void tearDown() {
extender.close();
}
public void testStoreFetch() throws NullCursorException, NonObjectJSONException, IOException, ParseException {
String guid = Utils.generateGuid();
extender.store(Utils.generateGuid(), null);
extender.store(guid, null);
extender.store(Utils.generateGuid(), null);
Cursor cur = null;
try {
cur = extender.fetch(guid);
assertEquals(1, cur.getCount());
assertTrue(cur.moveToFirst());
assertEquals(guid, cur.getString(0));
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testVisitsForGUID() throws NonArrayJSONException, NonObjectJSONException, IOException, ParseException, NullCursorException {
String guid = Utils.generateGuid();
JSONArray visits = new ExtendedJSONObject("{ \"visits\": [ { \"key\" : \"value\" } ] }").getArray("visits");
extender.store(Utils.generateGuid(), null);
extender.store(guid, visits);
extender.store(Utils.generateGuid(), null);
JSONArray fetchedVisits = extender.visitsForGUID(guid);
assertEquals(1, fetchedVisits.size());
assertEquals("value", ((JSONObject)fetchedVisits.get(0)).get("key"));
}
public void testDeleteHandlesBadGUIDs() {
String evilGUID = "' or '1'='1";
extender.store(Utils.generateGuid(), null);
extender.store(Utils.generateGuid(), null);
extender.store(evilGUID, null);
extender.delete(evilGUID);
Cursor cur = null;
try {
cur = extender.fetchAll();
assertEquals(cur.getCount(), 2);
assertTrue(cur.moveToFirst());
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, AndroidBrowserHistoryDataExtender.COL_GUID);
assertFalse(evilGUID.equals(guid));
cur.moveToNext();
}
} catch (NullCursorException e) {
e.printStackTrace();
fail("Should not have null cursor.");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testStoreFetchHandlesBadGUIDs() {
String evilGUID = "' or '1'='1";
extender.store(Utils.generateGuid(), null);
extender.store(Utils.generateGuid(), null);
extender.store(evilGUID, null);
Cursor cur = null;
try {
cur = extender.fetch(evilGUID);
assertEquals(1, cur.getCount());
assertTrue(cur.moveToFirst());
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, AndroidBrowserHistoryDataExtender.COL_GUID);
assertEquals(evilGUID, guid);
cur.moveToNext();
}
} catch (NullCursorException e) {
e.printStackTrace();
fail("Should not have null cursor.");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testBulkInsert() throws NullCursorException {
ArrayList<HistoryRecord> records = new ArrayList<HistoryRecord>();
records.add(HistoryHelpers.createHistory1());
records.add(HistoryHelpers.createHistory2());
extender.bulkInsert(records);
for (HistoryRecord record : records) {
HistoryRecord toCompare = (HistoryRecord) record.copyWithIDs(record.guid, record.androidID);
toCompare.visits = extender.visitsForGUID(record.guid);
assertEquals(record.visits.size(), toCompare.visits.size());
assertTrue(record.equals(toCompare));
}
// Now insert existing records, changing one, and add another record.
records.get(0).title = "test";
records.add(HistoryHelpers.createHistory3());
extender.bulkInsert(records);
for (HistoryRecord record : records) {
HistoryRecord toCompare = (HistoryRecord) record.copyWithIDs(record.guid, record.androidID);
toCompare.visits = extender.visitsForGUID(record.guid);
assertEquals(record.visits.size(), toCompare.visits.size());
assertTrue(record.equals(toCompare));
}
}
}

Просмотреть файл

@ -0,0 +1,498 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.ArrayList;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate;
import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataAccessor;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepositorySession;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositorySession;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
public class TestAndroidBrowserHistoryRepository extends AndroidBrowserRepositoryTestCase {
@Override
protected AndroidBrowserRepository getRepository() {
/**
* Override this chain in order to avoid our test code having to create two
* sessions all the time.
*/
return new AndroidBrowserHistoryRepository() {
@Override
protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
AndroidBrowserHistoryRepositorySession session;
session = new AndroidBrowserHistoryRepositorySession(this, context) {
@Override
protected synchronized void trackGUID(String guid) {
System.out.println("Ignoring trackGUID call: this is a test!");
}
};
delegate.onSessionCreated(session);
}
};
}
@Override
protected AndroidBrowserRepositoryDataAccessor getDataAccessor() {
return new AndroidBrowserHistoryDataAccessor(getApplicationContext());
}
@Override
protected void closeDataAccessor(AndroidBrowserRepositoryDataAccessor dataAccessor) {
if (!(dataAccessor instanceof AndroidBrowserHistoryDataAccessor)) {
throw new IllegalArgumentException("Only expecting a history data accessor.");
}
((AndroidBrowserHistoryDataAccessor) dataAccessor).closeExtender();
}
@Override
public void testFetchAll() {
Record[] expected = new Record[2];
expected[0] = HistoryHelpers.createHistory3();
expected[1] = HistoryHelpers.createHistory2();
basicFetchAllTest(expected);
}
/*
* Test storing identical records with different guids.
* For bookmarks identical is defined by the following fields
* being the same: title, uri, type, parentName
*/
@Override
public void testStoreIdenticalExceptGuid() {
storeIdenticalExceptGuid(HistoryHelpers.createHistory1());
}
@Override
public void testCleanMultipleRecords() {
cleanMultipleRecords(
HistoryHelpers.createHistory1(),
HistoryHelpers.createHistory2(),
HistoryHelpers.createHistory3(),
HistoryHelpers.createHistory4(),
HistoryHelpers.createHistory5()
);
}
@Override
public void testGuidsSinceReturnMultipleRecords() {
HistoryRecord record0 = HistoryHelpers.createHistory1();
HistoryRecord record1 = HistoryHelpers.createHistory2();
guidsSinceReturnMultipleRecords(record0, record1);
}
@Override
public void testGuidsSinceReturnNoRecords() {
guidsSinceReturnNoRecords(HistoryHelpers.createHistory3());
}
@Override
public void testFetchSinceOneRecord() {
fetchSinceOneRecord(HistoryHelpers.createHistory1(),
HistoryHelpers.createHistory2());
}
@Override
public void testFetchSinceReturnNoRecords() {
fetchSinceReturnNoRecords(HistoryHelpers.createHistory3());
}
@Override
public void testFetchOneRecordByGuid() {
fetchOneRecordByGuid(HistoryHelpers.createHistory1(),
HistoryHelpers.createHistory2());
}
@Override
public void testFetchMultipleRecordsByGuids() {
HistoryRecord record0 = HistoryHelpers.createHistory1();
HistoryRecord record1 = HistoryHelpers.createHistory2();
HistoryRecord record2 = HistoryHelpers.createHistory3();
fetchMultipleRecordsByGuids(record0, record1, record2);
}
@Override
public void testFetchNoRecordByGuid() {
fetchNoRecordByGuid(HistoryHelpers.createHistory1());
}
@Override
public void testWipe() {
doWipe(HistoryHelpers.createHistory2(), HistoryHelpers.createHistory3());
}
@Override
public void testStore() {
basicStoreTest(HistoryHelpers.createHistory1());
}
@Override
public void testRemoteNewerTimeStamp() {
HistoryRecord local = HistoryHelpers.createHistory1();
HistoryRecord remote = HistoryHelpers.createHistory2();
remoteNewerTimeStamp(local, remote);
}
@Override
public void testLocalNewerTimeStamp() {
HistoryRecord local = HistoryHelpers.createHistory1();
HistoryRecord remote = HistoryHelpers.createHistory2();
localNewerTimeStamp(local, remote);
}
@Override
public void testDeleteRemoteNewer() {
HistoryRecord local = HistoryHelpers.createHistory1();
HistoryRecord remote = HistoryHelpers.createHistory2();
deleteRemoteNewer(local, remote);
}
@Override
public void testDeleteLocalNewer() {
HistoryRecord local = HistoryHelpers.createHistory1();
HistoryRecord remote = HistoryHelpers.createHistory2();
deleteLocalNewer(local, remote);
}
@Override
public void testDeleteRemoteLocalNonexistent() {
deleteRemoteLocalNonexistent(HistoryHelpers.createHistory2());
}
/**
* Exists to provide access to record string logic.
*/
protected class HelperHistorySession extends AndroidBrowserHistoryRepositorySession {
public HelperHistorySession(Repository repository, Context context) {
super(repository, context);
}
public boolean sameRecordString(HistoryRecord r1, HistoryRecord r2) {
return buildRecordString(r1).equals(buildRecordString(r2));
}
}
/**
* Verifies that two history records with the same URI but different
* titles will be reconciled locally.
*/
public void testRecordStringCollisionAndEquality() {
final AndroidBrowserHistoryRepository repo = new AndroidBrowserHistoryRepository();
final HelperHistorySession testSession = new HelperHistorySession(repo, getApplicationContext());
final long now = RepositorySession.now();
final HistoryRecord record0 = new HistoryRecord(null, "history", now + 1, false);
final HistoryRecord record1 = new HistoryRecord(null, "history", now + 2, false);
final HistoryRecord record2 = new HistoryRecord(null, "history", now + 3, false);
record0.histURI = "http://example.com/foo";
record1.histURI = "http://example.com/foo";
record2.histURI = "http://example.com/bar";
record0.title = "Foo 0";
record1.title = "Foo 1";
record2.title = "Foo 2";
// Ensure that two records with the same URI produce the same record string,
// and two records with different URIs do not.
assertTrue(testSession.sameRecordString(record0, record1));
assertFalse(testSession.sameRecordString(record0, record2));
// Two records are congruent if they have the same URI and their
// identifiers match (which is why these all have null GUIDs).
assertTrue(record0.congruentWith(record0));
assertTrue(record0.congruentWith(record1));
assertTrue(record1.congruentWith(record0));
assertFalse(record0.congruentWith(record2));
assertFalse(record1.congruentWith(record2));
assertFalse(record2.congruentWith(record1));
assertFalse(record2.congruentWith(record0));
// None of these records are equal, because they have different titles.
// (Except for being equal to themselves, of course.)
assertTrue(record0.equalPayloads(record0));
assertTrue(record1.equalPayloads(record1));
assertTrue(record2.equalPayloads(record2));
assertFalse(record0.equalPayloads(record1));
assertFalse(record1.equalPayloads(record0));
assertFalse(record1.equalPayloads(record2));
}
/*
* Tests for adding some visits to a history record
* and doing a fetch.
*/
@SuppressWarnings("unchecked")
public void testAddOneVisit() {
final RepositorySession session = createAndBeginSession();
HistoryRecord record0 = HistoryHelpers.createHistory3();
performWait(storeRunnable(session, record0));
// Add one visit to the count and put in a new
// last visited date.
ContentValues cv = new ContentValues();
int visits = record0.visits.size() + 1;
long newVisitTime = System.currentTimeMillis();
cv.put(BrowserContract.History.VISITS, visits);
cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime);
final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor();
dataAccessor.updateByGuid(record0.guid, cv);
// Add expected visit to record for verification.
JSONObject expectedVisit = new JSONObject();
expectedVisit.put("date", newVisitTime * 1000); // Microseconds.
expectedVisit.put("type", 1L);
record0.visits.add(expectedVisit);
performWait(fetchRunnable(session, new String[] { record0.guid }, new ExpectFetchDelegate(new Record[] { record0 })));
closeDataAccessor(dataAccessor);
}
@SuppressWarnings("unchecked")
public void testAddMultipleVisits() {
final RepositorySession session = createAndBeginSession();
HistoryRecord record0 = HistoryHelpers.createHistory4();
performWait(storeRunnable(session, record0));
// Add three visits to the count and put in a new
// last visited date.
ContentValues cv = new ContentValues();
int visits = record0.visits.size() + 3;
long newVisitTime = System.currentTimeMillis();
cv.put(BrowserContract.History.VISITS, visits);
cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime);
final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor();
dataAccessor.updateByGuid(record0.guid, cv);
// Now shift to microsecond timing for visits.
long newMicroVisitTime = newVisitTime * 1000;
// Add expected visits to record for verification
JSONObject expectedVisit = new JSONObject();
expectedVisit.put("date", newMicroVisitTime);
expectedVisit.put("type", 1L);
record0.visits.add(expectedVisit);
expectedVisit = new JSONObject();
expectedVisit.put("date", newMicroVisitTime - 1000);
expectedVisit.put("type", 1L);
record0.visits.add(expectedVisit);
expectedVisit = new JSONObject();
expectedVisit.put("date", newMicroVisitTime - 2000);
expectedVisit.put("type", 1L);
record0.visits.add(expectedVisit);
ExpectFetchDelegate delegate = new ExpectFetchDelegate(new Record[] { record0 });
performWait(fetchRunnable(session, new String[] { record0.guid }, delegate));
Record fetched = delegate.records.get(0);
assertTrue(record0.equalPayloads(fetched));
closeDataAccessor(dataAccessor);
}
public void testInvalidHistoryItemIsSkipped() throws NullCursorException {
final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
final AndroidBrowserRepositoryDataAccessor dbHelper = session.getDBHelper();
final long now = System.currentTimeMillis();
final HistoryRecord emptyURL = new HistoryRecord(Utils.generateGuid(), "history", now, false);
final HistoryRecord noVisits = new HistoryRecord(Utils.generateGuid(), "history", now, false);
final HistoryRecord aboutURL = new HistoryRecord(Utils.generateGuid(), "history", now, false);
emptyURL.fennecDateVisited = now;
emptyURL.fennecVisitCount = 1;
emptyURL.histURI = "";
emptyURL.title = "Something";
noVisits.fennecDateVisited = now;
noVisits.fennecVisitCount = 0;
noVisits.histURI = "http://example.org/novisits";
noVisits.title = "Something Else";
aboutURL.fennecDateVisited = now;
aboutURL.fennecVisitCount = 1;
aboutURL.histURI = "about:home";
aboutURL.title = "Fennec Home";
Uri one = dbHelper.insert(emptyURL);
Uri two = dbHelper.insert(noVisits);
Uri tre = dbHelper.insert(aboutURL);
assertNotNull(one);
assertNotNull(two);
assertNotNull(tre);
// The records are in the DB.
final Cursor all = dbHelper.fetchAll();
assertEquals(3, all.getCount());
all.close();
// But aren't returned by fetching.
performWait(fetchAllRunnable(session, new Record[] {}));
// And we'd ignore about:home if we downloaded it.
assertTrue(session.shouldIgnore(aboutURL));
session.abort();
}
public void testSqlInjectPurgeDelete() {
// Some setup.
RepositorySession session = createAndBeginSession();
final AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
try {
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
// Create and insert 2 history entries, 2nd one is evil (attempts injection).
HistoryRecord h1 = HistoryHelpers.createHistory1();
HistoryRecord h2 = HistoryHelpers.createHistory2();
h2.guid = "' or '1'='1";
db.insert(h1);
db.insert(h2);
// Test 1 - updateByGuid() handles evil history entries correctly.
db.updateByGuid(h2.guid, cv);
// Query history table.
Cursor cur = getAllHistory();
int numHistory = cur.getCount();
// Ensure only the evil history entry is marked for deletion.
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
if (guid.equals(h2.guid)) {
assertTrue(deleted);
} else {
assertFalse(deleted);
}
cur.moveToNext();
}
} finally {
cur.close();
}
// Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record.
try {
db.purgeDeleted();
} catch (NullCursorException e) {
e.printStackTrace();
}
cur = getAllHistory();
int numHistoryAfterDeletion = cur.getCount();
// Ensure we have only 1 deleted row.
assertEquals(numHistoryAfterDeletion, numHistory - 1);
// Ensure only the evil history is deleted.
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
if (guid.equals(h2.guid)) {
fail("Evil guid was not deleted!");
} else {
assertFalse(deleted);
}
cur.moveToNext();
}
} finally {
cur.close();
}
} finally {
closeDataAccessor(db);
session.abort();
}
}
protected Cursor getAllHistory() {
Context context = getApplicationContext();
Cursor cur = context.getContentResolver().query(BrowserContractHelpers.HISTORY_CONTENT_URI,
BrowserContractHelpers.HistoryColumns, null, null, null);
return cur;
}
public void testDataAccessorBulkInsert() throws NullCursorException {
final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
ArrayList<HistoryRecord> records = new ArrayList<HistoryRecord>();
records.add(HistoryHelpers.createHistory1());
records.add(HistoryHelpers.createHistory2());
records.add(HistoryHelpers.createHistory3());
db.bulkInsert(records);
performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(records.toArray(new Record[records.size()]))));
session.abort();
}
public void testDataExtenderIsClosedBeforeBegin() {
// Create a session but don't begin() it.
final AndroidBrowserRepositorySession session = (AndroidBrowserRepositorySession) createSession();
AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
// Confirm dataExtender is closed before beginning session.
assertTrue(db.getHistoryDataExtender().isClosed());
}
public void testDataExtenderIsClosedAfterFinish() throws InactiveSessionException {
final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
// Perform an action that opens the dataExtender.
HistoryRecord h1 = HistoryHelpers.createHistory1();
db.insert(h1);
assertFalse(db.getHistoryDataExtender().isClosed());
// Check dataExtender is closed upon finish.
performWait(finishRunnable(session, new ExpectFinishDelegate()));
assertTrue(db.getHistoryDataExtender().isClosed());
}
public void testDataExtenderIsClosedAfterAbort() throws InactiveSessionException {
final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
// Perform an action that opens the dataExtender.
HistoryRecord h1 = HistoryHelpers.createHistory1();
db.insert(h1);
assertFalse(db.getHistoryDataExtender().isClosed());
// Check dataExtender is closed upon abort.
session.abort();
assertTrue(db.getHistoryDataExtender().isClosed());
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataExtender;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
import android.database.Cursor;
import android.test.AndroidTestCase;
public class TestCachedSQLiteOpenHelper extends AndroidTestCase {
protected ClientsDatabase clientsDB;
protected AndroidBrowserHistoryDataExtender extender;
public void setUp() {
clientsDB = new ClientsDatabase(mContext);
extender = new AndroidBrowserHistoryDataExtender(mContext);
}
public void tearDown() {
clientsDB.close();
extender.close();
}
public void testUnclosedDatabasesDontInteract() throws NullCursorException {
// clientsDB gracefully does its thing and closes.
clientsDB.wipeClientsTable();
ClientRecord record = new ClientRecord();
String profileConst = ClientsDatabaseAccessor.PROFILE_ID;
clientsDB.store(profileConst, record);
clientsDB.close();
// extender does its thing but still hasn't closed.
HistoryRecord h = HistoryHelpers.createHistory1();
extender.store(h.guid, h.visits);
// Ensure items in the clientsDB are still accessible nonetheless.
Cursor cur = null;
try {
cur = clientsDB.fetchAllClients();
assertTrue(cur.moveToFirst());
assertEquals(1, cur.getCount());
} finally {
if (cur != null) {
cur.close();
}
}
}
}

Просмотреть файл

@ -0,0 +1,198 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.ArrayList;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import android.database.Cursor;
import android.test.AndroidTestCase;
public class TestClientsDatabase extends AndroidTestCase {
protected ClientsDatabase db;
public void setUp() {
db = new ClientsDatabase(mContext);
db.wipeDB();
}
public void testStoreAndFetch() {
ClientRecord record = new ClientRecord();
String profileConst = ClientsDatabaseAccessor.PROFILE_ID;
db.store(profileConst, record);
Cursor cur = null;
try {
// Test stored item gets fetched correctly.
cur = db.fetchClientsCursor(record.guid, profileConst);
assertTrue(cur.moveToFirst());
assertEquals(1, cur.getCount());
String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
String profileId = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_PROFILE);
String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME);
String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE);
assertEquals(record.guid, guid);
assertEquals(profileConst, profileId);
assertEquals(record.name, clientName);
assertEquals(record.type, clientType);
} catch (NullCursorException e) {
fail("Should not have NullCursorException");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testStoreAndFetchSpecificCommands() {
String accountGUID = Utils.generateGuid();
ArrayList<String> args = new ArrayList<String>();
args.add("URI of Page");
args.add("Sender GUID");
args.add("Title of Page");
String jsonArgs = JSONArray.toJSONString(args);
Cursor cur = null;
try {
db.store(accountGUID, "displayURI", jsonArgs);
// This row should not show up in the fetch.
args.add("Another arg.");
db.store(accountGUID, "displayURI", JSONArray.toJSONString(args));
// Test stored item gets fetched correctly.
cur = db.fetchSpecificCommand(accountGUID, "displayURI", jsonArgs);
assertTrue(cur.moveToFirst());
assertEquals(1, cur.getCount());
String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
String commandType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_COMMAND);
String fetchedArgs = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ARGS);
assertEquals(accountGUID, guid);
assertEquals("displayURI", commandType);
assertEquals(jsonArgs, fetchedArgs);
} catch (NullCursorException e) {
fail("Should not have NullCursorException");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testFetchCommandsForClient() {
String accountGUID = Utils.generateGuid();
ArrayList<String> args = new ArrayList<String>();
args.add("URI of Page");
args.add("Sender GUID");
args.add("Title of Page");
String jsonArgs = JSONArray.toJSONString(args);
Cursor cur = null;
try {
db.store(accountGUID, "displayURI", jsonArgs);
// This row should ALSO show up in the fetch.
args.add("Another arg.");
db.store(accountGUID, "displayURI", JSONArray.toJSONString(args));
// Test both stored items with the same GUID but different command are fetched.
cur = db.fetchCommandsForClient(accountGUID);
assertTrue(cur.moveToFirst());
assertEquals(2, cur.getCount());
} catch (NullCursorException e) {
fail("Should not have NullCursorException");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testDelete() {
ClientRecord record1 = new ClientRecord();
ClientRecord record2 = new ClientRecord();
String profileConst = ClientsDatabaseAccessor.PROFILE_ID;
db.store(profileConst, record1);
db.store(profileConst, record2);
Cursor cur = null;
try {
// Test record doesn't exist after delete.
db.deleteClient(record1.guid, profileConst);
cur = db.fetchClientsCursor(record1.guid, profileConst);
assertFalse(cur.moveToFirst());
assertEquals(0, cur.getCount());
// Test record2 still there after deleting record1.
cur = db.fetchClientsCursor(record2.guid, profileConst);
assertTrue(cur.moveToFirst());
assertEquals(1, cur.getCount());
String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID);
String profileId = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_PROFILE);
String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME);
String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE);
assertEquals(record2.guid, guid);
assertEquals(profileConst, profileId);
assertEquals(record2.name, clientName);
assertEquals(record2.type, clientType);
} catch (NullCursorException e) {
fail("Should not have NullCursorException");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testWipe() {
ClientRecord record1 = new ClientRecord();
ClientRecord record2 = new ClientRecord();
String profileConst = ClientsDatabaseAccessor.PROFILE_ID;
db.store(profileConst, record1);
db.store(profileConst, record2);
Cursor cur = null;
try {
// Test before wipe the records are there.
cur = db.fetchClientsCursor(record2.guid, profileConst);
assertTrue(cur.moveToFirst());
assertEquals(1, cur.getCount());
cur = db.fetchClientsCursor(record2.guid, profileConst);
assertTrue(cur.moveToFirst());
assertEquals(1, cur.getCount());
// Test after wipe neither record exists.
db.wipeClientsTable();
cur = db.fetchClientsCursor(record2.guid, profileConst);
assertFalse(cur.moveToFirst());
assertEquals(0, cur.getCount());
cur = db.fetchClientsCursor(record1.guid, profileConst);
assertFalse(cur.moveToFirst());
assertEquals(0, cur.getCount());
} catch (NullCursorException e) {
fail("Should not have NullCursorException");
} finally {
if (cur != null) {
cur.close();
}
}
}
}

Просмотреть файл

@ -0,0 +1,128 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.mozilla.gecko.background.testhelpers.CommandHelpers;
import org.mozilla.gecko.sync.CommandProcessor.Command;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import android.content.Context;
import android.database.Cursor;
import android.test.AndroidTestCase;
public class TestClientsDatabaseAccessor extends AndroidTestCase {
public class StubbedClientsDatabaseAccessor extends ClientsDatabaseAccessor {
public StubbedClientsDatabaseAccessor(Context mContext) {
super(mContext);
}
}
StubbedClientsDatabaseAccessor db;
public void setUp() {
db = new StubbedClientsDatabaseAccessor(mContext);
db.wipeDB();
}
public void tearDown() {
db.close();
}
public void testStoreArrayListAndFetch() throws NullCursorException {
ArrayList<ClientRecord> list = new ArrayList<ClientRecord>();
ClientRecord record1 = new ClientRecord(Utils.generateGuid());
ClientRecord record2 = new ClientRecord(Utils.generateGuid());
ClientRecord record3 = new ClientRecord(Utils.generateGuid());
list.add(record1);
list.add(record2);
db.store(list);
ClientRecord r1 = db.fetchClient(record1.guid);
ClientRecord r2 = db.fetchClient(record2.guid);
ClientRecord r3 = db.fetchClient(record3.guid);
assertNotNull(r1);
assertNotNull(r2);
assertNull(r3);
assertTrue(record1.equals(r1));
assertTrue(record2.equals(r2));
assertFalse(record3.equals(r3));
}
public void testStoreAndFetchCommandsForClient() {
String accountGUID1 = Utils.generateGuid();
String accountGUID2 = Utils.generateGuid();
Command command1 = CommandHelpers.getCommand1();
Command command2 = CommandHelpers.getCommand2();
Command command3 = CommandHelpers.getCommand3();
Cursor cur = null;
try {
db.store(accountGUID1, command1);
db.store(accountGUID1, command2);
db.store(accountGUID2, command3);
List<Command> commands = db.fetchCommandsForClient(accountGUID1);
assertEquals(2, commands.size());
assertEquals(1, commands.get(0).args.size());
assertEquals(1, commands.get(1).args.size());
} catch (NullCursorException e) {
fail("Should not have NullCursorException");
} finally {
if (cur != null) {
cur.close();
}
}
}
public void testNumClients() {
final int COUNT = 5;
ArrayList<ClientRecord> list = new ArrayList<ClientRecord>();
for (int i = 0; i < 5; i++) {
list.add(new ClientRecord());
}
db.store(list);
assertEquals(COUNT, db.clientsCount());
}
public void testFetchAll() throws NullCursorException {
ArrayList<ClientRecord> list = new ArrayList<ClientRecord>();
ClientRecord record1 = new ClientRecord(Utils.generateGuid());
ClientRecord record2 = new ClientRecord(Utils.generateGuid());
list.add(record1);
list.add(record2);
boolean thrown = false;
try {
Map<String, ClientRecord> records = db.fetchAllClients();
assertNotNull(records);
assertEquals(0, records.size());
db.store(list);
records = db.fetchAllClients();
assertNotNull(records);
assertEquals(2, records.size());
assertTrue(record1.equals(records.get(record1.guid)));
assertTrue(record2.equals(records.get(record2.guid)));
// put() should throw an exception since records is immutable.
records.put(null, null);
} catch (UnsupportedOperationException e) {
thrown = true;
}
assertTrue(thrown);
}
}

Просмотреть файл

@ -0,0 +1,202 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import org.json.simple.JSONArray;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.repositories.NoContentProviderException;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository.FennecTabsRepositorySession;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.domain.Record;
import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.os.RemoteException;
public class TestFennecTabsRepositorySession extends AndroidSyncTestCase {
public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces.
public static final String TEST_CLIENT_NAME = "test client name";
// Override these to test against data that is not live.
public static final String TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS ?";
public static final String[] TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID };
protected ContentProviderClient tabsClient = null;
protected ContentProviderClient getTabsClient() {
final ContentResolver cr = getApplicationContext().getContentResolver();
return cr.acquireContentProviderClient(BrowserContract.Tabs.CONTENT_URI);
}
public TestFennecTabsRepositorySession() throws NoContentProviderException {
super();
}
public void setUp() {
if (tabsClient == null) {
tabsClient = getTabsClient();
}
}
protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
if (tabsClient == null) {
return -1;
}
return tabsClient.delete(BrowserContract.Tabs.CONTENT_URI,
TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS);
}
protected void tearDown() throws Exception {
if (tabsClient != null) {
deleteAllTestTabs(tabsClient);
tabsClient.release();
tabsClient = null;
}
}
protected FennecTabsRepository getRepository() {
/**
* Override this chain in order to avoid our test code having to create two
* sessions all the time.
*/
return new FennecTabsRepository(TEST_CLIENT_NAME, TEST_CLIENT_GUID) {
@Override
public void createSession(RepositorySessionCreationDelegate delegate,
Context context) {
try {
final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context) {
@Override
protected synchronized void trackGUID(String guid) {
}
@Override
protected String localClientSelection() {
return TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION;
}
@Override
protected String[] localClientSelectionArgs() {
return TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS;
}
};
delegate.onSessionCreated(session);
} catch (Exception e) {
delegate.onSessionCreateFailed(e);
}
}
};
}
protected FennecTabsRepositorySession createSession() {
return (FennecTabsRepositorySession) SessionTestHelper.createSession(
getApplicationContext(),
getRepository());
}
protected FennecTabsRepositorySession createAndBeginSession() {
return (FennecTabsRepositorySession) SessionTestHelper.createAndBeginSession(
getApplicationContext(),
getRepository());
}
protected Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final Record[] expectedRecords) {
return new Runnable() {
@Override
public void run() {
session.fetchSince(timestamp, new ExpectFetchDelegate(expectedRecords));
}
};
}
protected Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) {
return new Runnable() {
@Override
public void run() {
session.fetchAll(new ExpectFetchDelegate(expectedRecords));
}
};
}
protected Tab testTab1;
protected Tab testTab2;
protected Tab testTab3;
@SuppressWarnings("unchecked")
private void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
final JSONArray history1 = new JSONArray();
history1.add("http://test.com/test1.html");
testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
final JSONArray history2 = new JSONArray();
history2.add("http://test.com/test2.html#1");
history2.add("http://test.com/test2.html#2");
history2.add("http://test.com/test2.html#3");
testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
final JSONArray history3 = new JSONArray();
history3.add("http://test.com/test3.html#1");
history3.add("http://test.com/test3.html#2");
testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000);
tabsClient.insert(BrowserContract.Tabs.CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0));
tabsClient.insert(BrowserContract.Tabs.CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1));
tabsClient.insert(BrowserContract.Tabs.CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2));
}
protected TabsRecord insertTestTabsAndExtractTabsRecord() throws RemoteException {
insertSomeTestTabs(tabsClient);
final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
Cursor cursor = null;
try {
cursor = tabsClient.query(BrowserContract.Tabs.CONTENT_URI, null,
TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS, positionAscending);
CursorDumper.dumpCursor(cursor);
final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
assertNotNull(tabsRecord.tabs);
assertEquals(cursor.getCount(), tabsRecord.tabs.size());
return tabsRecord;
} finally {
cursor.close();
}
}
public void testFetchAll() throws NoContentProviderException, RemoteException {
final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord();
final FennecTabsRepositorySession session = createAndBeginSession();
performWait(fetchAllRunnable(session, new Record[] { tabsRecord }));
session.abort();
}
public void testFetchSince() throws NoContentProviderException, RemoteException {
final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord();
final FennecTabsRepositorySession session = createAndBeginSession();
// Not all tabs are modified after this, but the record should contain them all.
performWait(fetchSinceRunnable(session, 1000, new Record[] { tabsRecord }));
// No tabs are modified after this, so we shouldn't get a record at all.
performWait(fetchSinceRunnable(session, 4000, new Record[] { }));
session.abort();
}
}

Просмотреть файл

@ -0,0 +1,195 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import org.json.simple.JSONArray;
import org.mozilla.gecko.db.BrowserContract;
import android.app.Activity;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.test.ActivityInstrumentationTestCase2;
/**
* Exercise Fennec's tabs provider.
*
* @author rnewman
*
*/
public class TestFennecTabsStorage extends ActivityInstrumentationTestCase2<Activity> {
public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces.
public static final String TEST_CLIENT_NAME = "test client name";
public static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?";
protected Tab testTab1;
protected Tab testTab2;
protected Tab testTab3;
public TestFennecTabsStorage() {
super(Activity.class);
}
protected ContentProviderClient getClientsClient() {
final ContentResolver cr = getInstrumentation().getTargetContext().getApplicationContext().getContentResolver();
return cr.acquireContentProviderClient(BrowserContract.Clients.CONTENT_URI);
}
protected ContentProviderClient getTabsClient() {
final ContentResolver cr = getInstrumentation().getTargetContext().getApplicationContext().getContentResolver();
return cr.acquireContentProviderClient(BrowserContract.Tabs.CONTENT_URI);
}
protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException {
if (tabsClient == null) {
return -1;
}
return tabsClient.delete(BrowserContract.Tabs.CONTENT_URI, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID });
}
protected void tearDown() throws Exception {
deleteAllTestTabs(getTabsClient());
}
@SuppressWarnings("unchecked")
protected void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException {
final JSONArray history1 = new JSONArray();
history1.add("http://test.com/test1.html");
testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
final JSONArray history2 = new JSONArray();
history2.add("http://test.com/test2.html#1");
history2.add("http://test.com/test2.html#2");
history2.add("http://test.com/test2.html#3");
testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
final JSONArray history3 = new JSONArray();
history3.add("http://test.com/test3.html#1");
history3.add("http://test.com/test3.html#2");
testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000);
tabsClient.insert(BrowserContract.Tabs.CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0));
tabsClient.insert(BrowserContract.Tabs.CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1));
tabsClient.insert(BrowserContract.Tabs.CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2));
}
// Sanity.
public void testObtainCP() {
final ContentProviderClient clientsClient = getClientsClient();
assertNotNull(clientsClient);
clientsClient.release();
final ContentProviderClient tabsClient = getTabsClient();
assertNotNull(tabsClient);
tabsClient.release();
}
public void testWipeClients() throws RemoteException {
final Uri uri = BrowserContract.Clients.CONTENT_URI;
final ContentProviderClient clientsClient = getClientsClient();
// Have to ensure that it's empty
clientsClient.delete(uri, null, null);
int deleted = clientsClient.delete(uri, null, null);
assertEquals(0, deleted);
}
public void testWipeTabs() throws RemoteException {
final ContentProviderClient tabsClient = getTabsClient();
// Have to ensure that it's empty
deleteAllTestTabs(tabsClient);
int deleted = deleteAllTestTabs(tabsClient);
assertEquals(0, deleted);
}
public void testStoreAndRetrieveClients() throws RemoteException {
final Uri uri = BrowserContract.Clients.CONTENT_URI;
final ContentProviderClient clientsClient = getClientsClient();
// Have to ensure that it's empty
clientsClient.delete(uri, null, null);
final long now = System.currentTimeMillis();
final ContentValues first = new ContentValues();
final ContentValues second = new ContentValues();
first.put(BrowserContract.Clients.GUID, "abcdefghijkl");
first.put(BrowserContract.Clients.NAME, "Frist Psot");
first.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
second.put(BrowserContract.Clients.GUID, "mnopqrstuvwx");
second.put(BrowserContract.Clients.NAME, "Second!!1!");
second.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
ContentValues[] values = new ContentValues[] { first, second };
final int inserted = clientsClient.bulkInsert(uri, values);
assertEquals(2, inserted);
final String since = BrowserContract.Clients.LAST_MODIFIED + " >= ?";
final String[] nowArg = new String[] { String.valueOf(now) };
final String guidAscending = BrowserContract.Clients.GUID + " ASC";
Cursor cursor = clientsClient.query(uri, null, since, nowArg, guidAscending);
assertNotNull(cursor);
try {
assertTrue(cursor.moveToFirst());
assertEquals(2, cursor.getCount());
final String g1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
final String n1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
final long m1 = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
assertEquals(first.get(BrowserContract.Clients.GUID), g1);
assertEquals(first.get(BrowserContract.Clients.NAME), n1);
assertEquals(now + 1, m1);
assertTrue(cursor.moveToNext());
final String g2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID));
final String n2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME));
final long m2 = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED));
assertEquals(second.get(BrowserContract.Clients.GUID), g2);
assertEquals(second.get(BrowserContract.Clients.NAME), n2);
assertEquals(now + 2, m2);
assertFalse(cursor.moveToNext());
} finally {
cursor.close();
}
int deleted = clientsClient.delete(uri, null, null);
assertEquals(2, deleted);
}
public void testTabFromCursor() throws Exception {
final ContentProviderClient tabsClient = getTabsClient();
deleteAllTestTabs(tabsClient);
insertSomeTestTabs(tabsClient);
final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
Cursor cursor = null;
try {
cursor = tabsClient.query(BrowserContract.Tabs.CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
assertEquals(3, cursor.getCount());
cursor.moveToFirst();
final Tab parsed1 = Tab.fromCursor(cursor);
assertEquals(testTab1, parsed1);
cursor.moveToNext();
final Tab parsed2 = Tab.fromCursor(cursor);
assertEquals(testTab2, parsed2);
cursor.moveToPosition(2);
final Tab parsed3 = Tab.fromCursor(cursor);
assertEquals(testTab3, parsed3);
} finally {
cursor.close();
}
}
}

Просмотреть файл

@ -0,0 +1,448 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectStoreCompletedDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate;
import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.NoContentProviderException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
import org.mozilla.gecko.sync.repositories.android.FormHistoryRepositorySession;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
public class TestFormHistoryRepositorySession extends AndroidSyncTestCase {
protected ContentProviderClient formsProvider = null;
public TestFormHistoryRepositorySession() throws NoContentProviderException {
super();
}
public void setUp() {
if (formsProvider == null) {
try {
formsProvider = FormHistoryRepositorySession.acquireContentProvider(getApplicationContext());
} catch (NoContentProviderException e) {
fail("Failed to acquireContentProvider: " + e);
}
}
try {
FormHistoryRepositorySession.purgeDatabases(formsProvider);
} catch (RemoteException e) {
fail("Failed to purgeDatabases: " + e);
}
}
public void tearDown() {
if (formsProvider != null) {
formsProvider.release();
formsProvider = null;
}
}
protected FormHistoryRepositorySession.FormHistoryRepository getRepository() {
/**
* Override this chain in order to avoid our test code having to create two
* sessions all the time.
*/
return new FormHistoryRepositorySession.FormHistoryRepository() {
@Override
public void createSession(RepositorySessionCreationDelegate delegate,
Context context) {
try {
final FormHistoryRepositorySession session = new FormHistoryRepositorySession(this, context) {
@Override
protected synchronized void trackGUID(String guid) {
}
};
delegate.onSessionCreated(session);
} catch (Exception e) {
delegate.onSessionCreateFailed(e);
}
}
};
}
protected FormHistoryRepositorySession createSession() {
return (FormHistoryRepositorySession) SessionTestHelper.createSession(
getApplicationContext(),
getRepository());
}
protected FormHistoryRepositorySession createAndBeginSession() {
return (FormHistoryRepositorySession) SessionTestHelper.createAndBeginSession(
getApplicationContext(),
getRepository());
}
public void testAcquire() throws NoContentProviderException {
final FormHistoryRepositorySession session = createAndBeginSession();
assertNotNull(session.getFormsProvider());
session.abort();
}
protected int numRecords(FormHistoryRepositorySession session, Uri uri) throws RemoteException {
Cursor cur = null;
try {
cur = session.getFormsProvider().query(uri, null, null, null, null);
return cur.getCount();
} finally {
if (cur != null) {
cur.close();
}
}
}
protected long after0;
protected long after1;
protected long after2;
protected long after3;
protected long after4;
protected FormHistoryRecord regular1;
protected FormHistoryRecord regular2;
protected FormHistoryRecord deleted1;
protected FormHistoryRecord deleted2;
public void insertTwoRecords(FormHistoryRepositorySession session) throws RemoteException {
Uri regularUri = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI;
Uri deletedUri = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI;
after0 = System.currentTimeMillis();
regular1 = new FormHistoryRecord("guid1", "forms", System.currentTimeMillis(), false);
regular1.fieldName = "fieldName1";
regular1.fieldValue = "value1";
final ContentValues cv1 = new ContentValues();
cv1.put(BrowserContract.FormHistory.GUID, regular1.guid);
cv1.put(BrowserContract.FormHistory.FIELD_NAME, regular1.fieldName);
cv1.put(BrowserContract.FormHistory.VALUE, regular1.fieldValue);
cv1.put(BrowserContract.FormHistory.FIRST_USED, 1000 * regular1.lastModified); // Microseconds.
int regularInserted = session.getFormsProvider().bulkInsert(regularUri, new ContentValues[] { cv1 });
assertEquals(1, regularInserted);
after1 = System.currentTimeMillis();
deleted1 = new FormHistoryRecord("guid3", "forms", -1, true);
final ContentValues cv3 = new ContentValues();
cv3.put(BrowserContract.FormHistory.GUID, deleted1.guid);
// cv3.put(BrowserContract.DeletedFormHistory.TIME_DELETED, record3.lastModified); // Set by CP.
int deletedInserted = session.getFormsProvider().bulkInsert(deletedUri, new ContentValues[] { cv3 });
assertEquals(1, deletedInserted);
after2 = System.currentTimeMillis();
regular2 = null;
deleted2 = null;
}
public void insertFourRecords(FormHistoryRepositorySession session) throws RemoteException {
Uri regularUri = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI;
Uri deletedUri = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI;
insertTwoRecords(session);
regular2 = new FormHistoryRecord("guid2", "forms", System.currentTimeMillis(), false);
regular2.fieldName = "fieldName2";
regular2.fieldValue = "value2";
final ContentValues cv2 = new ContentValues();
cv2.put(BrowserContract.FormHistory.GUID, regular2.guid);
cv2.put(BrowserContract.FormHistory.FIELD_NAME, regular2.fieldName);
cv2.put(BrowserContract.FormHistory.VALUE, regular2.fieldValue);
cv2.put(BrowserContract.FormHistory.FIRST_USED, 1000 * regular2.lastModified); // Microseconds.
int regularInserted = session.getFormsProvider().bulkInsert(regularUri, new ContentValues[] { cv2 });
assertEquals(1, regularInserted);
after3 = System.currentTimeMillis();
deleted2 = new FormHistoryRecord("guid4", "forms", -1, true);
final ContentValues cv4 = new ContentValues();
cv4.put(BrowserContract.FormHistory.GUID, deleted2.guid);
// cv4.put(BrowserContract.DeletedFormHistory.TIME_DELETED, record4.lastModified); // Set by CP.
int deletedInserted = session.getFormsProvider().bulkInsert(deletedUri, new ContentValues[] { cv4 });
assertEquals(1, deletedInserted);
after4 = System.currentTimeMillis();
}
public void testWipe() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertTwoRecords(session);
assertTrue(numRecords(session, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI) > 0);
assertTrue(numRecords(session, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI) > 0);
performWait(WaitHelper.onThreadRunnable(new Runnable() {
@Override
public void run() {
session.wipe(new RepositorySessionWipeDelegate() {
public void onWipeSucceeded() {
performNotify();
}
public void onWipeFailed(Exception ex) {
performNotify("Wipe should have succeeded", ex);
}
@Override
public RepositorySessionWipeDelegate deferredWipeDelegate(final ExecutorService executor) {
return this;
}
});
}
}));
assertEquals(0, numRecords(session, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI));
assertEquals(0, numRecords(session, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI));
session.abort();
}
protected Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expectedGuids) {
return new Runnable() {
@Override
public void run() {
session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expectedGuids));
}
};
}
protected Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) {
return new Runnable() {
@Override
public void run() {
session.fetchAll(new ExpectFetchDelegate(expectedRecords));
}
};
}
protected Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expectedRecords) {
return new Runnable() {
@Override
public void run() {
try {
session.fetch(guids, new ExpectFetchDelegate(expectedRecords));
} catch (InactiveSessionException e) {
performNotify(e);
}
}
};
}
public void testFetchAll() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertTwoRecords(session);
performWait(fetchAllRunnable(session, new Record[] { regular1, deleted1 }));
session.abort();
}
public void testFetchByGuid() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertTwoRecords(session);
performWait(fetchRunnable(session,
new String[] { regular1.guid, deleted1.guid },
new Record[] { regular1, deleted1 }));
performWait(fetchRunnable(session,
new String[] { regular1.guid },
new Record[] { regular1 }));
performWait(fetchRunnable(session,
new String[] { deleted1.guid, "NON_EXISTENT_GUID?" },
new Record[] { deleted1 }));
performWait(fetchRunnable(session,
new String[] { "FIRST_NON_EXISTENT_GUID", "SECOND_NON_EXISTENT_GUID?" },
new Record[] { }));
session.abort();
}
public void testFetchSince() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertFourRecords(session);
performWait(fetchSinceRunnable(session,
after0, new String[] { regular1.guid, deleted1.guid, regular2.guid, deleted2.guid }));
performWait(fetchSinceRunnable(session,
after1, new String[] { deleted1.guid, regular2.guid, deleted2.guid }));
performWait(fetchSinceRunnable(session,
after2, new String[] { regular2.guid, deleted2.guid }));
performWait(fetchSinceRunnable(session,
after3, new String[] { deleted2.guid }));
performWait(fetchSinceRunnable(session,
after4, new String[] { }));
session.abort();
}
protected Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expectedGuids) {
return new Runnable() {
@Override
public void run() {
session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expectedGuids));
}
};
}
public void testGuidsSince() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertTwoRecords(session);
performWait(guidsSinceRunnable(session,
after0, new String[] { regular1.guid, deleted1.guid }));
performWait(guidsSinceRunnable(session,
after1, new String[] { deleted1.guid}));
performWait(guidsSinceRunnable(session,
after2, new String[] { }));
session.abort();
}
protected Runnable storeRunnable(final RepositorySession session, final Record record, final RepositorySessionStoreDelegate delegate) {
return new Runnable() {
@Override
public void run() {
session.setStoreDelegate(delegate);
try {
session.store(record);
session.storeDone();
} catch (NoStoreDelegateException e) {
performNotify("NoStoreDelegateException should not occur.", e);
}
}
};
}
public void testStoreRemoteNew() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertTwoRecords(session);
FormHistoryRecord rec;
// remote regular, local missing => should store.
rec = new FormHistoryRecord("new1", "forms", System.currentTimeMillis(), false);
rec.fieldName = "fieldName1";
rec.fieldValue = "fieldValue1";
performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { rec }));
// remote deleted, local missing => should delete, but at the moment we ignore.
rec = new FormHistoryRecord("new2", "forms", System.currentTimeMillis(), true);
performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { }));
session.abort();
}
public void testStoreRemoteNewer() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertFourRecords(session);
long newTimestamp = System.currentTimeMillis();
FormHistoryRecord rec;
// remote regular, local regular, remote newer => should update.
rec = new FormHistoryRecord(regular1.guid, regular1.collection, newTimestamp, false);
rec.fieldName = regular1.fieldName;
rec.fieldValue = regular1.fieldValue + "NEW";
performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
performWait(fetchRunnable(session, new String[] { regular1.guid }, new Record[] { rec }));
// remote deleted, local regular, remote newer => should delete everything.
rec = new FormHistoryRecord(regular2.guid, regular2.collection, newTimestamp, true);
performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
performWait(fetchRunnable(session, new String[] { regular2.guid }, new Record[] { }));
// remote regular, local deleted, remote newer => should update.
rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, newTimestamp, false);
rec.fieldName = regular1.fieldName;
rec.fieldValue = regular1.fieldValue + "NEW";
performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
performWait(fetchRunnable(session, new String[] { deleted1.guid }, new Record[] { rec }));
// remote deleted, local deleted, remote newer => should delete everything.
rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, newTimestamp, true);
performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
performWait(fetchRunnable(session, new String[] { deleted2.guid }, new Record[] { }));
session.abort();
}
public static class ExpectNoStoreDelegate extends ExpectStoreCompletedDelegate {
@Override
public void onRecordStoreSucceeded(String guid) {
performNotify("Should not have stored record " + guid, null);
}
}
public void testStoreRemoteOlder() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
long oldTimestamp = System.currentTimeMillis() - 100;
insertFourRecords(session);
FormHistoryRecord rec;
// remote regular, local regular, remote older => should ignore.
rec = new FormHistoryRecord(regular1.guid, regular1.collection, oldTimestamp, false);
rec.fieldName = regular1.fieldName;
rec.fieldValue = regular1.fieldValue + "NEW";
performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
// remote deleted, local regular, remote older => should ignore.
rec = new FormHistoryRecord(regular2.guid, regular2.collection, oldTimestamp, true);
performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
// remote regular, local deleted, remote older => should ignore.
rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, oldTimestamp, false);
rec.fieldName = regular1.fieldName;
rec.fieldValue = regular1.fieldValue + "NEW";
performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
// remote deleted, local deleted, remote older => should ignore.
rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, oldTimestamp, true);
performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate()));
session.abort();
}
public void testStoreDifferentGuid() throws NoContentProviderException, RemoteException {
final FormHistoryRepositorySession session = createAndBeginSession();
insertTwoRecords(session);
FormHistoryRecord rec = (FormHistoryRecord) regular1.copyWithIDs("distinct", 999);
performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid)));
// Existing record should take remote record's GUID.
performWait(fetchAllRunnable(session, new Record[] { rec, deleted1 }));
session.abort();
}
}

Просмотреть файл

@ -0,0 +1,398 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import java.util.HashSet;
import java.util.Set;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate;
import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate;
import org.mozilla.gecko.background.sync.helpers.PasswordHelpers;
import org.mozilla.gecko.background.sync.helpers.SessionTestHelper;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.os.RemoteException;
public class TestPasswordsRepository extends AndroidSyncTestCase {
private final String NEW_PASSWORD1 = "password";
private final String NEW_PASSWORD2 = "drowssap";
@Override
public void setUp() {
wipe();
assertTrue(WaitHelper.getTestWaiter().isIdle());
}
public void testFetchAll() {
RepositorySession session = createAndBeginSession();
Record[] expected = new Record[] { PasswordHelpers.createPassword1(),
PasswordHelpers.createPassword2() };
performWait(storeRunnable(session, expected[0]));
performWait(storeRunnable(session, expected[1]));
performWait(fetchAllRunnable(session, expected));
dispose(session);
}
public void testGuidsSinceReturnMultipleRecords() {
RepositorySession session = createAndBeginSession();
PasswordRecord record1 = PasswordHelpers.createPassword1();
PasswordRecord record2 = PasswordHelpers.createPassword2();
updatePassword(NEW_PASSWORD1, record1);
long timestamp = updatePassword(NEW_PASSWORD2, record2);
String[] expected = new String[] { record1.guid, record2.guid };
performWait(storeRunnable(session, record1));
performWait(storeRunnable(session, record2));
performWait(guidsSinceRunnable(session, timestamp, expected));
dispose(session);
}
public void testGuidsSinceReturnNoRecords() {
RepositorySession session = createAndBeginSession();
// Store 1 record in the past.
performWait(storeRunnable(session, PasswordHelpers.createPassword1()));
String[] expected = {};
performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected));
dispose(session);
}
public void testFetchSinceOneRecord() {
RepositorySession session = createAndBeginSession();
// Passwords fetchSince checks timePasswordChanged, not insertion time.
PasswordRecord record1 = PasswordHelpers.createPassword1();
long timeModified1 = updatePassword(NEW_PASSWORD1, record1);
performWait(storeRunnable(session, record1));
PasswordRecord record2 = PasswordHelpers.createPassword2();
long timeModified2 = updatePassword(NEW_PASSWORD2, record2);
performWait(storeRunnable(session, record2));
String[] expectedOne = new String[] { record2.guid };
performWait(fetchSinceRunnable(session, timeModified2 - 10, expectedOne));
String[] expectedBoth = new String[] { record1.guid, record2.guid };
performWait(fetchSinceRunnable(session, timeModified1 - 10, expectedBoth));
dispose(session);
}
public void testFetchSinceReturnNoRecords() {
RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, PasswordHelpers.createPassword2()));
long timestamp = System.currentTimeMillis();
performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {}));
dispose(session);
}
public void testFetchOneRecordByGuid() {
RepositorySession session = createAndBeginSession();
Record record = PasswordHelpers.createPassword1();
performWait(storeRunnable(session, record));
performWait(storeRunnable(session, PasswordHelpers.createPassword2()));
String[] guids = new String[] { record.guid };
Record[] expected = new Record[] { record };
performWait(fetchRunnable(session, guids, expected));
dispose(session);
}
public void testFetchMultipleRecordsByGuids() {
RepositorySession session = createAndBeginSession();
PasswordRecord record1 = PasswordHelpers.createPassword1();
PasswordRecord record2 = PasswordHelpers.createPassword2();
PasswordRecord record3 = PasswordHelpers.createPassword3();
performWait(storeRunnable(session, record1));
performWait(storeRunnable(session, record2));
performWait(storeRunnable(session, record3));
String[] guids = new String[] { record1.guid, record2.guid };
Record[] expected = new Record[] { record1, record2 };
performWait(fetchRunnable(session, guids, expected));
dispose(session);
}
public void testFetchNoRecordByGuid() {
RepositorySession session = createAndBeginSession();
Record record = PasswordHelpers.createPassword1();
performWait(storeRunnable(session, record));
performWait(fetchRunnable(session,
new String[] { Utils.generateGuid() },
new Record[] {}));
dispose(session);
}
public void testStore() {
final RepositorySession session = createAndBeginSession();
performWait(storeRunnable(session, PasswordHelpers.createPassword1()));
dispose(session);
}
public void testRemoteNewerTimeStamp() {
final RepositorySession session = createAndBeginSession();
// Store updated local record.
PasswordRecord local = PasswordHelpers.createPassword1();
updatePassword(NEW_PASSWORD1, local, System.currentTimeMillis() - 1000);
performWait(storeRunnable(session, local));
// Sync a remote record version that is newer.
PasswordRecord remote = PasswordHelpers.createPassword2();
remote.guid = local.guid;
updatePassword(NEW_PASSWORD2, remote);
performWait(storeRunnable(session, remote));
// Make a fetch, expecting only the newer (remote) record.
performWait(fetchAllRunnable(session, new Record[] { remote }));
dispose(session);
}
public void testLocalNewerTimeStamp() {
final RepositorySession session = createAndBeginSession();
// Remote record updated before local record.
PasswordRecord remote = PasswordHelpers.createPassword1();
updatePassword(NEW_PASSWORD1, remote, System.currentTimeMillis() - 1000);
// Store updated local record.
PasswordRecord local = PasswordHelpers.createPassword2();
updatePassword(NEW_PASSWORD2, local);
performWait(storeRunnable(session, local));
// Sync a remote record version that is older.
remote.guid = local.guid;
performWait(storeRunnable(session, remote));
// Make a fetch, expecting only the newer (local) record.
performWait(fetchAllRunnable(session, new Record[] { local }));
dispose(session);
}
/*
* Store two records that are identical except for guid. Expect to find the
* remote one after reconciling.
*/
public void testStoreIdenticalExceptGuid() {
RepositorySession session = createAndBeginSession();
PasswordRecord record = PasswordHelpers.createPassword1();
record.guid = "before1";
// Store record.
performWait(storeRunnable(session, record));
// Store same record, but with different guid.
record.guid = Utils.generateGuid();
performWait(storeRunnable(session, record));
performWait(fetchAllRunnable(session, new Record[] { record }));
dispose(session);
session = createAndBeginSession();
PasswordRecord record2 = PasswordHelpers.createPassword2();
record2.guid = "before2";
// Store record.
performWait(storeRunnable(session, record2));
// Store same record, but with different guid.
record2.guid = Utils.generateGuid();
performWait(storeRunnable(session, record2));
performWait(fetchAllRunnable(session, new Record[] { record, record2 }));
dispose(session);
}
/*
* Store two records that are identical except for guid when they both point
* to the same site and there are multiple records for that site. Expect to
* find the remote one after reconciling.
*/
public void testStoreIdenticalExceptGuidOnSameSite() {
RepositorySession session = createAndBeginSession();
PasswordRecord record1 = PasswordHelpers.createPassword1();
record1.encryptedUsername = "original";
record1.guid = "before1";
PasswordRecord record2 = PasswordHelpers.createPassword1();
record2.encryptedUsername = "different";
record1.guid = "before2";
// Store records.
performWait(storeRunnable(session, record1));
performWait(storeRunnable(session, record2));
performWait(fetchAllRunnable(session, new Record[] { record1, record2 }));
dispose(session);
session = createAndBeginSession();
// Store same records, but with different guids.
record1.guid = Utils.generateGuid();
performWait(storeRunnable(session, record1));
performWait(fetchAllRunnable(session, new Record[] { record1, record2 }));
record2.guid = Utils.generateGuid();
performWait(storeRunnable(session, record2));
performWait(fetchAllRunnable(session, new Record[] { record1, record2 }));
dispose(session);
}
public void testRawFetch() throws RemoteException {
RepositorySession session = createAndBeginSession();
Record[] expected = new Record[] { PasswordHelpers.createPassword1(),
PasswordHelpers.createPassword2() };
performWait(storeRunnable(session, expected[0]));
performWait(storeRunnable(session, expected[1]));
ContentProviderClient client = getApplicationContext().getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI);
Cursor cursor = client.query(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null, null, null);
assertEquals(2, cursor.getCount());
cursor.moveToFirst();
Set<String> guids = new HashSet<String>();
while (!cursor.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cursor, BrowserContract.Passwords.GUID);
guids.add(guid);
cursor.moveToNext();
}
cursor.close();
assertEquals(2, guids.size());
assertTrue(guids.contains(expected[0].guid));
assertTrue(guids.contains(expected[1].guid));
dispose(session);
}
// Helper methods.
private RepositorySession createAndBeginSession() {
return SessionTestHelper.createAndBeginSession(
getApplicationContext(),
getRepository());
}
private Repository getRepository() {
/**
* Override this chain in order to avoid our test code having to create two
* sessions all the time. Don't track records, so they filtering doesn't happen.
*/
return new PasswordsRepositorySession.PasswordsRepository() {
@Override
public void createSession(RepositorySessionCreationDelegate delegate,
Context context) {
PasswordsRepositorySession session;
session = new PasswordsRepositorySession(this, context) {
@Override
protected synchronized void trackGUID(String guid) {
}
};
delegate.onSessionCreated(session);
}
};
}
private void wipe() {
Context context = getApplicationContext();
context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null);
context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null);
}
private static void dispose(RepositorySession session) {
if (session != null) {
session.abort();
}
}
private static long updatePassword(String password, PasswordRecord record, long timestamp) {
record.encryptedPassword = password;
long modifiedTime = System.currentTimeMillis();
record.timePasswordChanged = record.lastModified = modifiedTime;
return modifiedTime;
}
private static long updatePassword(String password, PasswordRecord record) {
return updatePassword(password, record, System.currentTimeMillis());
}
// Runnable Helpers.
private static Runnable storeRunnable(final RepositorySession session, final Record record) {
return new Runnable() {
@Override
public void run() {
session.setStoreDelegate(new ExpectStoredDelegate(record.guid));
try {
session.store(record);
session.storeDone();
} catch (NoStoreDelegateException e) {
fail("NoStoreDelegateException should not occur.");
}
}
};
}
private static Runnable fetchAllRunnable(final RepositorySession session, final Record[] records) {
return new Runnable() {
@Override
public void run() {
session.fetchAll(new ExpectFetchDelegate(records));
}
};
}
private static Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
return new Runnable() {
@Override
public void run() {
session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expected));
}
};
}
private static Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) {
return new Runnable() {
@Override
public void run() {
session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expected));
}
};
}
private static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) {
return new Runnable() {
@Override
public void run() {
try {
session.fetch(guids, new ExpectFetchDelegate(expected));
} catch (InactiveSessionException e) {
performNotify(e);
}
}
};
}
}

Просмотреть файл

@ -0,0 +1,68 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment;
public class MockDatabaseEnvironment extends DatabaseEnvironment {
public MockDatabaseEnvironment(HealthReportDatabaseStorage storage, Class<? extends EnvironmentAppender> appender) {
super(storage, appender);
}
public MockDatabaseEnvironment(HealthReportDatabaseStorage storage) {
super(storage);
}
public static class MockEnvironmentAppender extends EnvironmentAppender {
public StringBuilder appended = new StringBuilder();
public MockEnvironmentAppender() {
super();
}
@Override
public void append(String s) {
appended.append(s);
}
@Override
public void append(int v) {
appended.append(v);
}
@Override
public String toString() {
return appended.toString();
}
}
public MockDatabaseEnvironment mockInit(String version) {
profileCreation = 1234;
cpuCount = 2;
memoryMB = 512;
isBlocklistEnabled = 1;
isTelemetryEnabled = 1;
extensionCount = 0;
pluginCount = 0;
themeCount = 0;
architecture = "";
sysName = "";
sysVersion = "";
vendor = "";
appName = "";
appID = "";
appVersion = version;
appBuildID = "";
platformVersion = "";
platformBuildID = "";
os = "";
xpcomabi = "";
updateChannel = "";
return this;
}
}

Просмотреть файл

@ -0,0 +1,275 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.util.ArrayList;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public class MockHealthReportDatabaseStorage extends HealthReportDatabaseStorage {
public long now = System.currentTimeMillis();
public long getOneDayAgo() {
return now - GlobalConstants.MILLISECONDS_PER_DAY;
}
public int getYesterday() {
return super.getDay(this.getOneDayAgo());
}
public int getToday() {
return super.getDay(now);
}
public int getTomorrow() {
return super.getDay(now + GlobalConstants.MILLISECONDS_PER_DAY);
}
public int getGivenDaysAgo(int numDays) {
return super.getDay(this.getGivenDaysAgoMillis(numDays));
}
public long getGivenDaysAgoMillis(int numDays) {
return now - numDays * GlobalConstants.MILLISECONDS_PER_DAY;
}
public MockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) {
super(context, fakeProfileDirectory);
}
public SQLiteDatabase getDB() {
return this.helper.getWritableDatabase();
}
@Override
public MockDatabaseEnvironment getEnvironment() {
return new MockDatabaseEnvironment(this);
}
@Override
public int deleteEnvAndEventsBefore(long time, int curEnv) {
return super.deleteEnvAndEventsBefore(time, curEnv);
}
@Override
public int deleteOrphanedEnv(int curEnv) {
return super.deleteOrphanedEnv(curEnv);
}
@Override
public int deleteEventsBefore(String dayString) {
return super.deleteEventsBefore(dayString);
}
@Override
public int deleteOrphanedAddons() {
return super.deleteOrphanedAddons();
}
@Override
public int getIntFromQuery(final String sql, final String[] selectionArgs) {
return super.getIntFromQuery(sql, selectionArgs);
}
/**
* A storage instance prepopulated with dummy data to be used for testing.
*
* Modifying this data directly will cause tests relying on it to fail so use the versioned
* constructor to change the data if it's the desired version. Example:
* <pre>
* if (version >= 3) {
* addVersion3Stuff();
* }
* if (version >= 2) {
* addVersion2Stuff();
* }
* addVersion1Stuff();
* </pre>
*
* Don't forget to increment the {@link MAX_VERSION_USED} constant.
*
* Note that all instances of this class use the same underlying database and so each newly
* created instance will share the same data.
*/
public static class PrepopulatedMockHealthReportDatabaseStorage extends MockHealthReportDatabaseStorage {
// A constant to enforce which version constructor is the maximum used so far.
private int MAX_VERSION_USED = 2;
public String[] measurementNames;
public int[] measurementVers;
public FieldSpecContainer[] fieldSpecContainers;
public int env;
private final JSONObject addonJSON = new JSONObject(
"{ " +
"\"amznUWL2@amazon.com\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.10\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15269, " +
" \"updateDay\": 15602 " +
"}, " +
"\"jid0-qBnIpLfDFa4LpdrjhAC6vBqN20Q@jetpack\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.12.1\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15062, " +
" \"updateDay\": 15580 " +
"} " +
"} ");
public static class FieldSpecContainer {
public final FieldSpec counter;
public final FieldSpec discrete;
public final FieldSpec last;
public FieldSpecContainer(FieldSpec counter, FieldSpec discrete, FieldSpec last) {
this.counter = counter;
this.discrete = discrete;
this.last = last;
}
public ArrayList<FieldSpec> asList() {
final ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(3);
out.add(counter);
out.add(discrete);
out.add(last);
return out;
}
}
public PrepopulatedMockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) throws Exception {
this(context, fakeProfileDirectory, 1);
}
public PrepopulatedMockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory, int version) throws Exception {
super(context, fakeProfileDirectory);
if (version > MAX_VERSION_USED || version < 1) {
throw new IllegalStateException("Invalid version number! Check " +
"PrepopulatedMockHealthReportDatabaseStorage.MAX_VERSION_USED!");
}
measurementNames = new String[2];
measurementNames[0] = "a_string_measurement";
measurementNames[1] = "b_integer_measurement";
measurementVers = new int[2];
measurementVers[0] = 1;
measurementVers[1] = 2;
fieldSpecContainers = new FieldSpecContainer[2];
fieldSpecContainers[0] = new FieldSpecContainer(
new FieldSpec("a_counter_integer_field", Field.TYPE_INTEGER_COUNTER),
new FieldSpec("a_discrete_string_field", Field.TYPE_STRING_DISCRETE),
new FieldSpec("a_last_string_field", Field.TYPE_STRING_LAST));
fieldSpecContainers[1] = new FieldSpecContainer(
new FieldSpec("b_counter_integer_field", Field.TYPE_INTEGER_COUNTER),
new FieldSpec("b_discrete_integer_field", Field.TYPE_INTEGER_DISCRETE),
new FieldSpec("b_last_integer_field", Field.TYPE_INTEGER_LAST));
final MeasurementFields[] measurementFields =
new MeasurementFields[fieldSpecContainers.length];
for (int i = 0; i < fieldSpecContainers.length; i++) {
final FieldSpecContainer fieldSpecContainer = fieldSpecContainers[i];
measurementFields[i] = new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
return fieldSpecContainer.asList();
}
};
}
this.beginInitialization();
for (int i = 0; i < measurementNames.length; i++) {
this.ensureMeasurementInitialized(measurementNames[i], measurementVers[i],
measurementFields[i]);
}
this.finishInitialization();
MockDatabaseEnvironment environment = this.getEnvironment();
environment.mockInit("v123");
environment.setJSONForAddons(addonJSON);
env = environment.register();
String mName = measurementNames[0];
int mVer = measurementVers[0];
FieldSpecContainer fieldSpecCont = fieldSpecContainers[0];
int fieldID = this.getField(mName, mVer, fieldSpecCont.counter.name).getID();
this.incrementDailyCount(env, this.getGivenDaysAgo(7), fieldID, 1);
this.incrementDailyCount(env, this.getGivenDaysAgo(4), fieldID, 2);
this.incrementDailyCount(env, this.getToday(), fieldID, 3);
fieldID = this.getField(mName, mVer, fieldSpecCont.discrete.name).getID();
this.recordDailyDiscrete(env, this.getGivenDaysAgo(5), fieldID, "five");
this.recordDailyDiscrete(env, this.getGivenDaysAgo(5), fieldID, "five-two");
this.recordDailyDiscrete(env, this.getGivenDaysAgo(2), fieldID, "two");
this.recordDailyDiscrete(env, this.getToday(), fieldID, "zero");
fieldID = this.getField(mName, mVer, fieldSpecCont.last.name).getID();
this.recordDailyLast(env, this.getGivenDaysAgo(6), fieldID, "six");
this.recordDailyLast(env, this.getGivenDaysAgo(3), fieldID, "three");
this.recordDailyLast(env, this.getToday(), fieldID, "zero");
mName = measurementNames[1];
mVer = measurementVers[1];
fieldSpecCont = fieldSpecContainers[1];
fieldID = this.getField(mName, mVer, fieldSpecCont.counter.name).getID();
this.incrementDailyCount(env, this.getGivenDaysAgo(2), fieldID, 2);
fieldID = this.getField(mName, mVer, fieldSpecCont.discrete.name).getID();
this.recordDailyDiscrete(env, this.getToday(), fieldID, 0);
this.recordDailyDiscrete(env, this.getToday(), fieldID, 1);
fieldID = this.getField(mName, mVer, fieldSpecCont.last.name).getID();
this.recordDailyLast(env, this.getYesterday(), fieldID, 1);
if (version >= 2) {
// Insert more diverse environments.
for (int i = 1; i <= 3; i++) {
environment = this.getEnvironment();
environment.mockInit("v" + i);
env = environment.register();
this.recordDailyLast(env, this.getGivenDaysAgo(7 * i + 1), fieldID, 13);
}
environment = this.getEnvironment();
environment.mockInit("v4");
env = environment.register();
this.recordDailyLast(env, this.getGivenDaysAgo(1000), fieldID, 14);
this.recordDailyLast(env, this.getToday(), fieldID, 15);
}
}
public void insertTextualEvents(final int count) {
final ContentValues v = new ContentValues();
v.put("env", env);
final int fieldID = this.getField(measurementNames[0], measurementVers[0],
fieldSpecContainers[0].discrete.name).getID();
v.put("field", fieldID);
v.put("value", "data");
final SQLiteDatabase db = this.helper.getWritableDatabase();
db.beginTransaction();
try {
for (int i = 1; i <= count; i++) {
v.put("date", i);
db.insertOrThrow("events_textual", null, v);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}
}

Просмотреть файл

@ -0,0 +1,172 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.HealthReportSQLiteOpenHelper;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public class MockHealthReportSQLiteOpenHelper extends HealthReportSQLiteOpenHelper {
private int version;
public MockHealthReportSQLiteOpenHelper(Context context, File fakeProfileDirectory, String name) {
super(context, fakeProfileDirectory, name);
version = HealthReportSQLiteOpenHelper.CURRENT_VERSION;
}
public MockHealthReportSQLiteOpenHelper(Context context, File fakeProfileDirectory, String name, int version) {
super(context, fakeProfileDirectory, name, version);
this.version = version;
}
@Override
public void onCreate(SQLiteDatabase db) {
if (version == HealthReportSQLiteOpenHelper.CURRENT_VERSION) {
super.onCreate(db);
} else if (version == 4) {
onCreateSchemaVersion4(db);
} else {
throw new IllegalStateException("Unknown version number, " + version + ".");
}
}
// Copy-pasta from HealthReportDatabaseStorage.onCreate from v4.
public void onCreateSchemaVersion4(SQLiteDatabase db) {
db.beginTransaction();
try {
db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" body TEXT, " +
" UNIQUE (body) " +
")");
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" hash TEXT, " +
" profileCreation INTEGER, " +
" cpuCount INTEGER, " +
" memoryMB INTEGER, " +
" isBlocklistEnabled INTEGER, " +
" isTelemetryEnabled INTEGER, " +
" extensionCount INTEGER, " +
" pluginCount INTEGER, " +
" themeCount INTEGER, " +
" architecture TEXT, " +
" sysName TEXT, " +
" sysVersion TEXT, " +
" vendor TEXT, " +
" appName TEXT, " +
" appID TEXT, " +
" appVersion TEXT, " +
" appBuildID TEXT, " +
" platformVersion TEXT, " +
" platformBuildID TEXT, " +
" os TEXT, " +
" xpcomabi TEXT, " +
" updateChannel TEXT, " +
" addonsID INTEGER, " +
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
" UNIQUE (hash) " +
")");
db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" name TEXT, " +
" version INTEGER, " +
" UNIQUE (name, version) " +
")");
db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" measurement INTEGER, " +
" name TEXT, " +
" flags INTEGER, " +
" FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
" UNIQUE (measurement, name)" +
")");
db.execSQL("CREATE TABLE events_integer (" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value INTEGER, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE TABLE events_textual (" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value TEXT, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
db.execSQL("CREATE VIEW events AS " +
"SELECT date, env, field, value FROM events_integer " +
"UNION ALL " +
"SELECT date, env, field, value FROM events_textual");
db.execSQL("CREATE VIEW named_events AS " +
"SELECT date, " +
" environments.hash AS environment, " +
" measurements.name AS measurement_name, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.flags AS field_flags, " +
" value FROM " +
"events JOIN environments ON events.env = environments.id " +
" JOIN fields ON events.field = fields.id " +
" JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW named_fields AS " +
"SELECT measurements.name AS measurement_name, " +
" measurements.id AS measurement_id, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.id AS field_id, " +
" fields.flags AS field_flags " +
"FROM fields JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW current_measurements AS " +
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
// createAddonsEnvironmentsView(db):
db.execSQL("CREATE VIEW environments_with_addons AS " +
"SELECT e.id AS id, " +
" e.hash AS hash, " +
" e.profileCreation AS profileCreation, " +
" e.cpuCount AS cpuCount, " +
" e.memoryMB AS memoryMB, " +
" e.isBlocklistEnabled AS isBlocklistEnabled, " +
" e.isTelemetryEnabled AS isTelemetryEnabled, " +
" e.extensionCount AS extensionCount, " +
" e.pluginCount AS pluginCount, " +
" e.themeCount AS themeCount, " +
" e.architecture AS architecture, " +
" e.sysName AS sysName, " +
" e.sysVersion AS sysVersion, " +
" e.vendor AS vendor, " +
" e.appName AS appName, " +
" e.appID AS appID, " +
" e.appVersion AS appVersion, " +
" e.appBuildID AS appBuildID, " +
" e.platformVersion AS platformVersion, " +
" e.platformBuildID AS platformBuildID, " +
" e.os AS os, " +
" e.xpcomabi AS xpcomabi, " +
" e.updateChannel AS updateChannel, " +
" addons.body AS addonsBody " +
"FROM environments AS e, addons " +
"WHERE e.addonsID = addons.id");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
public class MockProfileInformationCache extends ProfileInformationCache {
public MockProfileInformationCache(String profilePath) {
super(profilePath);
}
public boolean isInitialized() {
return this.initialized;
}
public boolean needsWrite() {
return this.needsWrite;
}
public File getFile() {
return this.file;
}
public void writeJSON(JSONObject toWrite) throws IOException {
writeToFile(toWrite);
}
public JSONObject readJSON() throws FileNotFoundException, JSONException {
return readFromFile();
}
}

Просмотреть файл

@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
public class TestEnvironmentBuilder extends FakeProfileTestCase {
public static void testIgnoringAddons() throws JSONException {
Environment env = new Environment() {
@Override
public int register() {
return 0;
}
};
JSONObject addons = new JSONObject();
JSONObject foo = new JSONObject();
foo.put("a", 1);
foo.put("b", "c");
addons.put("foo", foo);
JSONObject ignore = new JSONObject();
ignore.put("ignore", true);
addons.put("ig", ignore);
env.setJSONForAddons(addons);
JSONObject kept = env.getNonIgnoredAddons();
assertTrue(kept.has("foo"));
assertFalse(kept.has("ig"));
JSONObject fooCopy = kept.getJSONObject("foo");
assertSame(foo, fooCopy);
}
public void testSanity() throws IOException {
File subdir = new File(this.fakeProfileDirectory.getAbsolutePath() +
File.separator + "testPersisting");
subdir.mkdir();
long now = System.currentTimeMillis();
int expectedDays = (int) (now / GlobalConstants.MILLISECONDS_PER_DAY);
MockProfileInformationCache cache = new MockProfileInformationCache(subdir.getAbsolutePath());
assertFalse(cache.getFile().exists());
cache.beginInitialization();
cache.setBlocklistEnabled(true);
cache.setTelemetryEnabled(false);
cache.setProfileCreationTime(now);
cache.completeInitialization();
assertTrue(cache.getFile().exists());
Environment environment = EnvironmentBuilder.getCurrentEnvironment(cache);
assertEquals(AppConstants.MOZ_APP_BUILDID, environment.appBuildID);
assertEquals("Android", environment.os);
assertTrue(100 < environment.memoryMB); // Seems like a sane lower bound...
assertTrue(environment.cpuCount >= 1);
assertEquals(1, environment.isBlocklistEnabled);
assertEquals(0, environment.isTelemetryEnabled);
assertEquals(expectedDays, environment.profileCreation);
assertEquals(EnvironmentBuilder.getCurrentEnvironment(cache).getHash(),
environment.getHash());
cache.beginInitialization();
cache.setBlocklistEnabled(false);
cache.completeInitialization();
assertFalse(EnvironmentBuilder.getCurrentEnvironment(cache).getHash()
.equals(environment.getHash()));
}
@Override
protected String getCacheSuffix() {
return System.currentTimeMillis() + Math.random() + ".foo";
}
}

Просмотреть файл

@ -0,0 +1,145 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService;
import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
public class TestHealthReportBroadcastService
extends BackgroundServiceTestCase<TestHealthReportBroadcastService.MockHealthReportBroadcastService> {
public static class MockHealthReportBroadcastService extends HealthReportBroadcastService {
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName, GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
protected void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting Service thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
}
public TestHealthReportBroadcastService() {
super(MockHealthReportBroadcastService.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
// We can't mock AlarmManager since it has a package-private constructor, so instead we reset
// the alarm by hand.
cancelAlarm(getUploadIntent());
}
@Override
public void tearDown() throws Exception {
cancelAlarm(getUploadIntent());
super.tearDown();
}
protected Intent getUploadIntent() {
final Intent intent = new Intent(getContext(), HealthReportUploadService.class);
intent.setAction("upload");
return intent;
}
protected Intent getPruneIntent() {
final Intent intent = new Intent(getContext(), HealthReportPruneService.class);
intent.setAction("prune");
return intent;
}
public void testIgnoredUploadPrefIntents() throws Exception {
// Intent without "upload" extra is ignored.
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
barrier.reset();
// No "profileName" extra.
intent.putExtra("enabled", true)
.removeExtra("profileName");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
barrier.reset();
// No "profilePath" extra.
intent.putExtra("profileName", "profileName")
.removeExtra("profilePath");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
}
public void testUploadPrefIntentDisabled() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("enabled", false)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
}
public void testUploadPrefIntentEnabled() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("enabled", true)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertTrue(isServiceAlarmSet(getUploadIntent()));
}
public void testUploadServiceCancelled() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("enabled", true)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertTrue(isServiceAlarmSet(getUploadIntent()));
barrier.reset();
intent.putExtra("enabled", false);
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
}
public void testPruneService() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertTrue(isServiceAlarmSet(getPruneIntent()));
barrier.reset();
}
}

Просмотреть файл

@ -0,0 +1,653 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.util.ArrayList;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.healthreport.MockHealthReportDatabaseStorage.PrepopulatedMockHealthReportDatabaseStorage;
import org.mozilla.gecko.background.helpers.DBHelpers;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
public class TestHealthReportDatabaseStorage extends FakeProfileTestCase {
private String[] TABLE_NAMES = {
"addons",
"environments",
"measurements",
"fields",
"events_integer",
"events_textual"
};
@Override
protected String getCacheSuffix() {
return File.separator + "health-" + System.currentTimeMillis() + ".profile";
}
public static class MockMeasurementFields implements MeasurementFields {
@Override
public Iterable<FieldSpec> getFields() {
ArrayList<FieldSpec> fields = new ArrayList<FieldSpec>();
fields.add(new FieldSpec("testfield1", Field.TYPE_INTEGER_COUNTER));
fields.add(new FieldSpec("testfield2", Field.TYPE_INTEGER_COUNTER));
return fields;
}
}
public void testInitializingProvider() {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
storage.beginInitialization();
// Two providers with the same measurement and field names. Shouldn't conflict.
storage.ensureMeasurementInitialized("testpA.testm", 1, new MockMeasurementFields());
storage.ensureMeasurementInitialized("testpB.testm", 2, new MockMeasurementFields());
storage.finishInitialization();
// Now make sure our stuff is in the DB.
SQLiteDatabase db = storage.getDB();
Cursor c = db.query("measurements", new String[] {"id", "name", "version"}, null, null, null, null, "name");
assertTrue(c.moveToFirst());
assertEquals(2, c.getCount());
Object[][] expected = new Object[][] {
{null, "testpA.testm", 1},
{null, "testpB.testm", 2},
};
DBHelpers.assertCursorContains(expected, c);
c.close();
}
private static final JSONObject EXAMPLE_ADDONS = safeJSONObject(
"{ " +
"\"amznUWL2@amazon.com\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.10\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15269, " +
" \"updateDay\": 15602 " +
"}, " +
"\"jid0-qBnIpLfDFa4LpdrjhAC6vBqN20Q@jetpack\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.12.1\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15062, " +
" \"updateDay\": 15580 " +
"} " +
"} ");
private static JSONObject safeJSONObject(String s) {
try {
return new JSONObject(s);
} catch (JSONException e) {
return null;
}
}
public void testEnvironmentsAndFields() throws Exception {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
storage.beginInitialization();
storage.ensureMeasurementInitialized("testpA.testm", 1, new MockMeasurementFields());
storage.ensureMeasurementInitialized("testpB.testn", 1, new MockMeasurementFields());
storage.finishInitialization();
MockDatabaseEnvironment environmentA = storage.getEnvironment();
environmentA.mockInit("v123");
environmentA.setJSONForAddons(EXAMPLE_ADDONS);
final int envA = environmentA.register();
assertEquals(envA, environmentA.register());
// getField memoizes.
assertSame(storage.getField("foo", 2, "bar"),
storage.getField("foo", 2, "bar"));
// It throws if you refer to a non-existent field.
try {
storage.getField("foo", 2, "bar").getID();
fail("Should throw.");
} catch (IllegalStateException ex) {
// Expected.
}
// It returns the field ID for a valid field.
Field field = storage.getField("testpA.testm", 1, "testfield1");
assertTrue(field.getID() >= 0);
// These IDs are stable.
assertEquals(field.getID(), field.getID());
int fieldID = field.getID();
// Before inserting, no events.
assertFalse(storage.hasEventSince(0));
assertFalse(storage.hasEventSince(storage.now));
// Store some data for two environments across two days.
storage.incrementDailyCount(envA, storage.getYesterday(), fieldID, 4);
storage.incrementDailyCount(envA, storage.getYesterday(), fieldID, 1);
storage.incrementDailyCount(envA, storage.getToday(), fieldID, 2);
// After inserting, we have events.
assertTrue(storage.hasEventSince(storage.now - GlobalConstants.MILLISECONDS_PER_DAY));
assertTrue(storage.hasEventSince(storage.now));
// But not in the future.
assertFalse(storage.hasEventSince(storage.now + GlobalConstants.MILLISECONDS_PER_DAY));
MockDatabaseEnvironment environmentB = storage.getEnvironment();
environmentB.mockInit("v234");
environmentB.setJSONForAddons(EXAMPLE_ADDONS);
final int envB = environmentB.register();
assertFalse(envA == envB);
storage.incrementDailyCount(envB, storage.getToday(), fieldID, 6);
storage.incrementDailyCount(envB, storage.getToday(), fieldID, 2);
// Let's make sure everything's there.
Cursor c = storage.getRawEventsSince(storage.getOneDayAgo());
try {
assertTrue(c.moveToFirst());
assertTrue(assertRowEquals(c, storage.getYesterday(), envA, fieldID, 5));
assertTrue(assertRowEquals(c, storage.getToday(), envA, fieldID, 2));
assertFalse(assertRowEquals(c, storage.getToday(), envB, fieldID, 8));
} finally {
c.close();
}
// The stored environment has the provided JSON add-ons bundle.
Cursor e = storage.getEnvironmentRecordForID(envA);
e.moveToFirst();
assertEquals(EXAMPLE_ADDONS.toString(), e.getString(e.getColumnIndex("addonsBody")));
e.close();
e = storage.getEnvironmentRecordForID(envB);
e.moveToFirst();
assertEquals(EXAMPLE_ADDONS.toString(), e.getString(e.getColumnIndex("addonsBody")));
e.close();
// There's only one add-ons bundle in the DB, despite having two environments.
Cursor addons = storage.getDB().query("addons", null, null, null, null, null, null);
assertEquals(1, addons.getCount());
addons.close();
}
/**
* Asserts validity for a storage cursor. Returns whether there is another row to process.
*/
private static boolean assertRowEquals(Cursor c, int day, int env, int field, int value) {
assertEquals(day, c.getInt(0));
assertEquals(env, c.getInt(1));
assertEquals(field, c.getInt(2));
assertEquals(value, c.getLong(3));
return c.moveToNext();
}
/**
* Test robust insertions. This also acts as a test for the getPrepopulatedStorage method,
* allowing faster debugging if this fails and other tests relying on getPrepopulatedStorage
* also fail.
*/
public void testInsertions() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertNotNull(storage);
}
public void testForeignKeyConstraints() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final int envID = storage.getEnvironment().register();
final int counterFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].counter.name).getID();
final int discreteFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].discrete.name).getID();
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
final int nonExistentFieldID = DBHelpers.getNonExistentID(db, "fields");
final int nonExistentAddonID = DBHelpers.getNonExistentID(db, "addons");
final int nonExistentMeasurementID = DBHelpers.getNonExistentID(db, "measurements");
ContentValues v = new ContentValues();
v.put("field", counterFieldID);
v.put("env", nonExistentEnvID);
try {
db.insertOrThrow("events_integer", null, v);
fail("Should throw - events_integer(env) is referencing non-existent environments(id)");
} catch (SQLiteConstraintException e) { }
v.put("field", discreteFieldID);
try {
db.insertOrThrow("events_textual", null, v);
fail("Should throw - events_textual(env) is referencing non-existent environments(id)");
} catch (SQLiteConstraintException e) { }
v.put("field", nonExistentFieldID);
v.put("env", envID);
try {
db.insertOrThrow("events_integer", null, v);
fail("Should throw - events_integer(field) is referencing non-existent fields(id)");
} catch (SQLiteConstraintException e) { }
try {
db.insertOrThrow("events_textual", null, v);
fail("Should throw - events_textual(field) is referencing non-existent fields(id)");
} catch (SQLiteConstraintException e) { }
v = new ContentValues();
v.put("addonsID", nonExistentAddonID);
try {
db.insertOrThrow("environments", null, v);
fail("Should throw - environments(addonsID) is referencing non-existent addons(id).");
} catch (SQLiteConstraintException e) { }
v = new ContentValues();
v.put("measurement", nonExistentMeasurementID);
try {
db.insertOrThrow("fields", null, v);
fail("Should throw - fields(measurement) is referencing non-existent measurements(id).");
} catch (SQLiteConstraintException e) { }
}
private int getTotalEventCount(HealthReportStorage storage) {
final Cursor c = storage.getEventsSince(0);
try {
return c.getCount();
} finally {
c.close();
}
}
public void testCascadingDeletions() throws Exception {
PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
SQLiteDatabase db = storage.getDB();
db.delete("environments", null, null);
assertEquals(0, DBHelpers.getRowCount(db, "events_integer"));
assertEquals(0, DBHelpers.getRowCount(db, "events_textual"));
storage = new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
db = storage.getDB();
db.delete("measurements", null, null);
assertEquals(0, DBHelpers.getRowCount(db, "fields"));
assertEquals(0, DBHelpers.getRowCount(db, "events_integer"));
assertEquals(0, DBHelpers.getRowCount(db, "events_textual"));
}
public void testRestrictedDeletions() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
SQLiteDatabase db = storage.getDB();
try {
db.delete("addons", null, null);
fail("Should throw - environment references addons and thus addons cannot be deleted.");
} catch (SQLiteConstraintException e) { }
}
public void testDeleteEverything() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
storage.deleteEverything();
final SQLiteDatabase db = storage.getDB();
for (String table : TABLE_NAMES) {
if (DBHelpers.getRowCount(db, table) != 0) {
fail("Not everything has been deleted for table " + table + ".");
}
}
}
public void testMeasurementRecordingConstraintViolation() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final int envID = storage.getEnvironment().register();
final int counterFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].counter.name).getID();
final int discreteFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].discrete.name).getID();
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
final int nonExistentFieldID = DBHelpers.getNonExistentID(db, "fields");
try {
storage.incrementDailyCount(nonExistentEnvID, storage.getToday(), counterFieldID);
fail("Should throw - event_integer(env) references environments(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.recordDailyDiscrete(nonExistentEnvID, storage.getToday(), discreteFieldID, "iu");
fail("Should throw - event_textual(env) references environments(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.recordDailyLast(nonExistentEnvID, storage.getToday(), discreteFieldID, "iu");
fail("Should throw - event_textual(env) references environments(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.incrementDailyCount(envID, storage.getToday(), nonExistentFieldID);
fail("Should throw - event_integer(field) references fields(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.recordDailyDiscrete(envID, storage.getToday(), nonExistentFieldID, "iu");
fail("Should throw - event_textual(field) references fields(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.recordDailyLast(envID, storage.getToday(), nonExistentFieldID, "iu");
fail("Should throw - event_textual(field) references fields(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
}
// Largely taken from testDeleteEnvAndEventsBefore and testDeleteOrphanedAddons.
public void testDeleteDataBefore() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// Insert (and delete) an environment not referenced by any events.
ContentValues v = new ContentValues();
v.put("hash", "I really hope this is a unique hash! ^_^");
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
db.insertOrThrow("environments", null, v);
v.put("hash", "Another unique hash!");
final int curEnv = (int) db.insertOrThrow("environments", null, v);
final ContentValues addonV = new ContentValues();
addonV.put("body", "addon1");
db.insertOrThrow("addons", null, addonV);
// 2 = 1 addon + 1 env.
assertEquals(2, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8), curEnv));
assertEquals(1, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8),
DBHelpers.getNonExistentID(db, "environments")));
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
// Insert (and delete) new environment and referencing events.
final long envID = db.insertOrThrow("environments", null, v);
v = new ContentValues();
v.put("date", storage.getGivenDaysAgo(9));
v.put("env", envID);
v.put("field", DBHelpers.getExistentID(db, "fields"));
db.insertOrThrow("events_integer", null, v);
db.insertOrThrow("events_integer", null, v);
assertEquals(16, getTotalEventCount(storage));
final int nonExistentEnvID = (int) DBHelpers.getNonExistentID(db, "environments");
assertEquals(1, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8), nonExistentEnvID));
assertEquals(14, getTotalEventCount(storage));
// Assert only pre-populated storage is stored.
assertEquals(1, DBHelpers.getRowCount(db, "environments"));
assertEquals(0, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(5), nonExistentEnvID));
assertEquals(12, getTotalEventCount(storage));
assertEquals(0, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(4), nonExistentEnvID));
assertEquals(10, getTotalEventCount(storage));
assertEquals(0, storage.deleteDataBefore(storage.now, nonExistentEnvID));
assertEquals(5, getTotalEventCount(storage));
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
// 2 = 1 addon + 1 env.
assertEquals(2, storage.deleteDataBefore(storage.now + GlobalConstants.MILLISECONDS_PER_DAY,
nonExistentEnvID));
assertEquals(0, getTotalEventCount(storage));
assertEquals(0, DBHelpers.getRowCount(db, "addons"));
}
// Largely taken from testDeleteOrphanedEnv and testDeleteEventsBefore.
public void testDeleteEnvAndEventsBefore() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// Insert (and delete) an environment not referenced by any events.
ContentValues v = new ContentValues();
v.put("hash", "I really hope this is a unique hash! ^_^");
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
db.insertOrThrow("environments", null, v);
v.put("hash", "Another unique hash!");
final int curEnv = (int) db.insertOrThrow("environments", null, v);
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8), curEnv));
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8),
DBHelpers.getNonExistentID(db, "environments")));
// Insert (and delete) new environment and referencing events.
final long envID = db.insertOrThrow("environments", null, v);
v = new ContentValues();
v.put("date", storage.getGivenDaysAgo(9));
v.put("env", envID);
v.put("field", DBHelpers.getExistentID(db, "fields"));
db.insertOrThrow("events_integer", null, v);
db.insertOrThrow("events_integer", null, v);
assertEquals(16, getTotalEventCount(storage));
final int nonExistentEnvID = (int) DBHelpers.getNonExistentID(db, "environments");
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8), nonExistentEnvID));
assertEquals(14, getTotalEventCount(storage));
// Assert only pre-populated storage is stored.
assertEquals(1, DBHelpers.getRowCount(db, "environments"));
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(5), nonExistentEnvID));
assertEquals(12, getTotalEventCount(storage));
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(4), nonExistentEnvID));
assertEquals(10, getTotalEventCount(storage));
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.now, nonExistentEnvID));
assertEquals(5, getTotalEventCount(storage));
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.now + GlobalConstants.MILLISECONDS_PER_DAY,
nonExistentEnvID));
assertEquals(0, getTotalEventCount(storage));
}
public void testDeleteOrphanedEnv() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final ContentValues v = new ContentValues();
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
v.put("hash", "unique");
final int envID = (int) db.insert("environments", null, v);
assertEquals(0, storage.deleteOrphanedEnv(envID));
assertEquals(1, storage.deleteOrphanedEnv(storage.env));
this.deleteEvents(db);
assertEquals(1, storage.deleteOrphanedEnv(envID));
}
private void deleteEvents(final SQLiteDatabase db) throws Exception {
db.beginTransaction();
try {
db.delete("events_integer", null, null);
db.delete("events_textual", null, null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void testDeleteEventsBefore() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertEquals(2, storage.deleteEventsBefore(Integer.toString(storage.getGivenDaysAgo(5))));
assertEquals(12, getTotalEventCount(storage));
assertEquals(2, storage.deleteEventsBefore(Integer.toString(storage.getGivenDaysAgo(4))));
assertEquals(10, getTotalEventCount(storage));
assertEquals(5, storage.deleteEventsBefore(Integer.toString(storage.getToday())));
assertEquals(5, getTotalEventCount(storage));
assertEquals(5, storage.deleteEventsBefore(Integer.toString(storage.getTomorrow())));
assertEquals(0, getTotalEventCount(storage));
}
public void testDeleteOrphanedAddons() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final ArrayList<Integer> nonOrphanIDs = new ArrayList<Integer>();
final Cursor c = db.query("addons", new String[] {"id"}, null, null, null, null, null);
try {
assertTrue(c.moveToFirst());
do {
nonOrphanIDs.add(c.getInt(0));
} while (c.moveToNext());
} finally {
c.close();
}
// Ensure we don't delete non-orphans.
assertEquals(0, storage.deleteOrphanedAddons());
// Insert orphans.
final long[] orphanIDs = new long[2];
final ContentValues v = new ContentValues();
v.put("body", "addon1");
orphanIDs[0] = db.insertOrThrow("addons", null, v);
v.put("body", "addon2");
orphanIDs[1] = db.insertOrThrow("addons", null, v);
assertEquals(2, storage.deleteOrphanedAddons());
assertEquals(0, DBHelpers.getRowCount(db, "addons", "ID = ? OR ID = ?",
new String[] {Long.toString(orphanIDs[0]), Long.toString(orphanIDs[1])}));
// Orphan all addons.
db.delete("environments", null, null);
assertEquals(nonOrphanIDs.size(), storage.deleteOrphanedAddons());
assertEquals(0, DBHelpers.getRowCount(db, "addons"));
}
public void testGetEventCount() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertEquals(14, storage.getEventCount());
final SQLiteDatabase db = storage.getDB();
this.deleteEvents(db);
assertEquals(0, storage.getEventCount());
}
public void testGetEnvironmentCount() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertEquals(1, storage.getEnvironmentCount());
final SQLiteDatabase db = storage.getDB();
db.delete("environments", null, null);
assertEquals(0, storage.getEnvironmentCount());
}
public void testPruneEnvironments() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory, 2);
final SQLiteDatabase db = storage.getDB();
assertEquals(5, DBHelpers.getRowCount(db, "environments"));
storage.pruneEnvironments(1);
assertTrue(!getEnvAppVersions(db).contains("v3"));
storage.pruneEnvironments(2);
assertTrue(!getEnvAppVersions(db).contains("v2"));
assertTrue(!getEnvAppVersions(db).contains("v1"));
storage.pruneEnvironments(1);
assertTrue(!getEnvAppVersions(db).contains("v123"));
storage.pruneEnvironments(1);
assertTrue(!getEnvAppVersions(db).contains("v4"));
}
private ArrayList<String> getEnvAppVersions(final SQLiteDatabase db) {
ArrayList<String> out = new ArrayList<String>();
Cursor c = null;
try {
c = db.query(true, "environments", new String[] {"appVersion"}, null, null, null, null, null, null);
while (c.moveToNext()) {
out.add(c.getString(0));
}
} finally {
if (c != null) {
c.close();
}
}
return out;
}
public void testPruneEvents() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
SQLiteDatabase db = storage.getDB();
assertEquals(14, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(1); // Delete < 7 days ago.
assertEquals(14, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(2); // Delete < 5 days ago.
assertEquals(13, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(5); // Delete < 2 days ago.
assertEquals(9, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(14); // Delete < today.
assertEquals(5, DBHelpers.getRowCount(db, "events"));
}
public void testVacuum() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// Need to disable auto_vacuum to allow free page fragmentation. Note that the pragma changes
// only after a vacuum command.
db.execSQL("PRAGMA auto_vacuum=0");
db.execSQL("vacuum");
assertTrue(isAutoVacuumingDisabled(storage));
createFreePages(storage);
storage.vacuum();
assertEquals(0, getFreelistCount(storage));
}
public long getFreelistCount(final MockHealthReportDatabaseStorage storage) {
return storage.getIntFromQuery("PRAGMA freelist_count", null);
}
public boolean isAutoVacuumingDisabled(final MockHealthReportDatabaseStorage storage) {
return storage.getIntFromQuery("PRAGMA auto_vacuum", null) == 0;
}
private void createFreePages(final PrepopulatedMockHealthReportDatabaseStorage storage) throws Exception {
// Insert and delete until DB has free page fragmentation. The loop helps ensure that the
// fragmentation will occur with minimal disk usage. The upper loop limits are arbitrary.
final SQLiteDatabase db = storage.getDB();
for (int i = 10; i <= 1250; i *= 5) {
storage.insertTextualEvents(i);
db.delete("events_textual", "date < ?", new String[] {Integer.toString(i / 2)});
if (getFreelistCount(storage) > 0) {
return;
}
}
fail("Database free pages failed to fragment.");
}
public void testDisableAutoVacuuming() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// The pragma changes only after a vacuum command.
db.execSQL("PRAGMA auto_vacuum=1");
db.execSQL("vacuum");
assertEquals(1, storage.getIntFromQuery("PRAGMA auto_vacuum", null));
storage.disableAutoVacuuming();
db.execSQL("vacuum");
assertTrue(isAutoVacuumingDisabled(storage));
}
}

Просмотреть файл

@ -0,0 +1,409 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.DateUtils;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
public class TestHealthReportGenerator extends FakeProfileTestCase {
@SuppressWarnings("static-method")
public void testOptObject() throws JSONException {
JSONObject o = new JSONObject();
o.put("foo", JSONObject.NULL);
assertEquals(null, o.optJSONObject("foo"));
}
@SuppressWarnings("static-method")
public void testAppend() throws JSONException {
JSONObject o = new JSONObject();
HealthReportUtils.append(o, "yyy", 5);
assertNotNull(o.getJSONArray("yyy"));
assertEquals(5, o.getJSONArray("yyy").getInt(0));
o.put("foo", "noo");
HealthReportUtils.append(o, "foo", "bar");
assertNotNull(o.getJSONArray("foo"));
assertEquals("noo", o.getJSONArray("foo").getString(0));
assertEquals("bar", o.getJSONArray("foo").getString(1));
}
@SuppressWarnings("static-method")
public void testCount() throws JSONException {
JSONObject o = new JSONObject();
HealthReportUtils.count(o, "foo", "a");
HealthReportUtils.count(o, "foo", "b");
HealthReportUtils.count(o, "foo", "a");
HealthReportUtils.count(o, "foo", "c");
HealthReportUtils.count(o, "bar", "a");
HealthReportUtils.count(o, "bar", "d");
JSONObject foo = o.getJSONObject("foo");
JSONObject bar = o.getJSONObject("bar");
assertEquals(2, foo.getInt("a"));
assertEquals(1, foo.getInt("b"));
assertEquals(1, foo.getInt("c"));
assertFalse(foo.has("d"));
assertEquals(1, bar.getInt("a"));
assertEquals(1, bar.getInt("d"));
assertFalse(bar.has("b"));
}
private static final String EXPECTED_MOCK_BASE_HASH = "000nullnullnullnullnullnullnull"
+ "nullnullnullnullnullnull00000";
public void testHashing() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
MockDatabaseEnvironment env = new MockDatabaseEnvironment(storage, MockDatabaseEnvironment.MockEnvironmentAppender.class);
env.addons = new JSONObject();
String addonAHash = "{addonA}={appDisabled==falseforeignInstall==false"
+ "hasBinaryComponents==falseinstallDay==15269scope==1"
+ "type==extensionupdateDay==15602userDisabled==false"
+ "version==1.10}";
JSONObject addonA1 = new JSONObject("{" +
"\"userDisabled\": false, " +
"\"appDisabled\": false, " +
"\"version\": \"1.10\", " +
"\"type\": \"extension\", " +
"\"scope\": 1, " +
"\"foreignInstall\": false, " +
"\"hasBinaryComponents\": false, " +
"\"installDay\": 15269, " +
"\"updateDay\": 15602 " +
"}");
// A reordered but otherwise equivalent object.
JSONObject addonA1rev = new JSONObject("{" +
"\"userDisabled\": false, " +
"\"foreignInstall\": false, " +
"\"hasBinaryComponents\": false, " +
"\"installDay\": 15269, " +
"\"type\": \"extension\", " +
"\"scope\": 1, " +
"\"appDisabled\": false, " +
"\"version\": \"1.10\", " +
"\"updateDay\": 15602 " +
"}");
env.addons.put("{addonA}", addonA1);
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash, env.getHash());
env.addons.put("{addonA}", addonA1rev);
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash, env.getHash());
}
private void assertJSONDiff(JSONObject source, JSONObject diff) throws JSONException {
assertEquals(source.get("a"), diff.get("a"));
assertFalse(diff.has("b"));
assertEquals(source.get("c"), diff.get("c"));
JSONObject diffD = diff.getJSONObject("d");
assertFalse(diffD.has("aa"));
assertEquals(1, diffD.getJSONArray("bb").getInt(0));
JSONObject diffCC = diffD.getJSONObject("cc");
assertEquals(1, diffCC.length());
assertEquals(1, diffCC.getInt("---"));
}
private static void assertJSONEquals(JSONObject one, JSONObject two) throws JSONException {
if (one == null || two == null) {
assertEquals(two, one);
}
assertEquals(one.length(), two.length());
@SuppressWarnings("unchecked")
Iterator<String> it = one.keys();
while (it.hasNext()) {
String key = it.next();
Object v1 = one.get(key);
Object v2 = two.get(key);
if (v1 instanceof JSONObject) {
assertTrue(v2 instanceof JSONObject);
assertJSONEquals((JSONObject) v1, (JSONObject) v2);
} else {
assertEquals(v1, v2);
}
}
}
@SuppressWarnings("static-method")
public void testNulls() {
assertTrue(JSONObject.NULL.equals(null));
assertTrue(JSONObject.NULL.equals(JSONObject.NULL));
assertFalse(JSONObject.NULL.equals(new JSONObject()));
assertFalse(null == JSONObject.NULL);
}
public void testJSONDiffing() throws JSONException {
String one = "{\"a\": 1, \"b\": 2, \"c\": [1, 2, 3], \"d\": {\"aa\": 5, \"bb\": [], \"cc\": {\"aaa\": null}}, \"e\": {}}";
String two = "{\"a\": 2, \"b\": 2, \"c\": [1, null, 3], \"d\": {\"aa\": 5, \"bb\": [1], \"cc\": {\"---\": 1, \"aaa\": null}}}";
JSONObject jOne = new JSONObject(one);
JSONObject jTwo = new JSONObject(two);
JSONObject diffNull = HealthReportGenerator.diff(jOne, jTwo, true);
JSONObject diffNoNull = HealthReportGenerator.diff(jOne, jTwo, false);
assertJSONDiff(jTwo, diffNull);
assertJSONDiff(jTwo, diffNoNull);
assertTrue(diffNull.isNull("e"));
assertFalse(diffNoNull.has("e"));
// Diffing to null returns the negation object: all the same keys but all null values.
JSONObject negated = new JSONObject("{\"a\": null, \"b\": null, \"c\": null, \"d\": null, \"e\": null}");
JSONObject toNull = HealthReportGenerator.diff(jOne, null, true);
assertJSONEquals(toNull, negated);
// Diffing from null returns the destination object.
JSONObject fromNull = HealthReportGenerator.diff(null, jOne, true);
assertJSONEquals(fromNull, jOne);
}
public void testAddonDiffing() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(
context,
fakeProfileDirectory);
final MockDatabaseEnvironment env1 = storage.getEnvironment();
env1.mockInit("23");
final MockDatabaseEnvironment env2 = storage.getEnvironment();
env2.mockInit("23");
env1.addons = new JSONObject();
env2.addons = new JSONObject();
JSONObject addonA1 = new JSONObject("{" + "\"userDisabled\": false, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.10\", "
+ "\"type\": \"extension\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": false, "
+ "\"installDay\": 15269, "
+ "\"updateDay\": 15602 " + "}");
JSONObject addonA2 = new JSONObject("{" + "\"userDisabled\": false, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.20\", "
+ "\"type\": \"extension\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": false, "
+ "\"installDay\": 15269, "
+ "\"updateDay\": 17602 " + "}");
JSONObject addonB1 = new JSONObject("{" + "\"userDisabled\": false, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.0\", "
+ "\"type\": \"theme\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": false, "
+ "\"installDay\": 10269, "
+ "\"updateDay\": 10002 " + "}");
JSONObject addonC1 = new JSONObject("{" + "\"userDisabled\": true, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.50\", "
+ "\"type\": \"plugin\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": true, "
+ "\"installDay\": 12269, "
+ "\"updateDay\": 12602 " + "}");
env1.addons.put("{addonA}", addonA1);
env1.addons.put("{addonB}", addonB1);
env2.addons.put("{addonA}", addonA2);
env2.addons.put("{addonB}", addonB1);
env2.addons.put("{addonC}", addonC1);
JSONObject env2JSON = HealthReportGenerator.jsonify(env2, env1);
JSONObject addons = env2JSON.getJSONObject("org.mozilla.addons.active");
assertTrue(addons.has("{addonA}"));
assertFalse(addons.has("{addonB}")); // Because it's unchanged.
assertTrue(addons.has("{addonC}"));
JSONObject aJSON = addons.getJSONObject("{addonA}");
assertEquals(2, aJSON.length());
assertEquals("1.20", aJSON.getString("version"));
assertEquals(17602, aJSON.getInt("updateDay"));
JSONObject cJSON = addons.getJSONObject("{addonC}");
assertEquals(9, cJSON.length());
}
public void testEnvironments() throws JSONException {
// Hard-coded so you need to update tests!
// If this is the only thing you need to change when revving a version, you
// need more test coverage.
final int expectedVersion = 3;
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
HealthReportGenerator gen = new HealthReportGenerator(storage);
final MockDatabaseEnvironment env1 = storage.getEnvironment();
env1.mockInit("23");
final String env1Hash = env1.getHash();
long now = System.currentTimeMillis();
JSONObject document = gen.generateDocument(0, 0, env1);
String today = new DateUtils.DateFormatter().getDateString(now);
assertFalse(document.has("lastPingDate"));
document = gen.generateDocument(0, HealthReportConstants.EARLIEST_LAST_PING, env1);
assertEquals("2013-05-02", document.get("lastPingDate"));
// True unless test spans midnight...
assertEquals(today, document.get("thisPingDate"));
assertEquals(expectedVersion, document.get("version"));
JSONObject environments = document.getJSONObject("environments");
JSONObject current = environments.getJSONObject("current");
assertTrue(current.has("org.mozilla.profile.age"));
assertTrue(current.has("org.mozilla.sysinfo.sysinfo"));
assertTrue(current.has("org.mozilla.appInfo.appinfo"));
assertTrue(current.has("geckoAppInfo"));
assertTrue(current.has("org.mozilla.addons.active"));
assertTrue(current.has("org.mozilla.addons.counts"));
// Make sure we don't get duplicate environments when an environment has
// been used, and that we get deltas between them.
env1.register();
final MockDatabaseEnvironment env2 = storage.getEnvironment();
env2.mockInit("24");
final String env2Hash = env2.getHash();
assertFalse(env2Hash.equals(env1Hash));
env2.register();
assertEquals(env2Hash, env2.getHash());
assertEquals("2013-05-02", document.get("lastPingDate"));
// True unless test spans midnight...
assertEquals(today, document.get("thisPingDate"));
assertEquals(expectedVersion, document.get("version"));
document = gen.generateDocument(0, HealthReportConstants.EARLIEST_LAST_PING, env2);
environments = document.getJSONObject("environments");
// Now we have two: env1, and env2 (as 'current').
assertTrue(environments.has(env1.getHash()));
assertTrue(environments.has("current"));
assertEquals(2, environments.length());
current = environments.getJSONObject("current");
assertTrue(current.has("org.mozilla.profile.age"));
assertTrue(current.has("org.mozilla.sysinfo.sysinfo"));
assertTrue(current.has("org.mozilla.appInfo.appinfo"));
assertTrue(current.has("geckoAppInfo"));
assertTrue(current.has("org.mozilla.addons.active"));
assertTrue(current.has("org.mozilla.addons.counts"));
// The diff only contains the changed measurement and fields.
JSONObject previous = environments.getJSONObject(env1.getHash());
assertTrue(previous.has("geckoAppInfo"));
final JSONObject previousAppInfo = previous.getJSONObject("geckoAppInfo");
assertEquals(2, previousAppInfo.length());
assertEquals("23", previousAppInfo.getString("version"));
assertEquals(Integer.valueOf(1), (Integer) previousAppInfo.get("_v"));
assertFalse(previous.has("org.mozilla.profile.age"));
assertFalse(previous.has("org.mozilla.sysinfo.sysinfo"));
assertFalse(previous.has("org.mozilla.appInfo.appinfo"));
assertFalse(previous.has("org.mozilla.addons.active"));
assertFalse(previous.has("org.mozilla.addons.counts"));
}
public void testInsertedData() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
HealthReportGenerator gen = new HealthReportGenerator(storage);
storage.beginInitialization();
final MockDatabaseEnvironment environment = storage.getEnvironment();
String envHash = environment.getHash();
int env = environment.mockInit("23").register();
storage.ensureMeasurementInitialized("org.mozilla.testm5", 1, new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
ArrayList<FieldSpec> out = new ArrayList<FieldSpec>();
out.add(new FieldSpec("counter", Field.TYPE_INTEGER_COUNTER));
out.add(new FieldSpec("discrete_int", Field.TYPE_INTEGER_DISCRETE));
out.add(new FieldSpec("discrete_str", Field.TYPE_STRING_DISCRETE));
out.add(new FieldSpec("last_int", Field.TYPE_INTEGER_LAST));
out.add(new FieldSpec("last_str", Field.TYPE_STRING_LAST));
out.add(new FieldSpec("counted_str", Field.TYPE_COUNTED_STRING_DISCRETE));
out.add(new FieldSpec("discrete_json", Field.TYPE_JSON_DISCRETE));
return out;
}
});
storage.finishInitialization();
long now = System.currentTimeMillis();
int day = storage.getDay(now);
final String todayString = new DateUtils.DateFormatter().getDateString(now);
int counter = storage.getField("org.mozilla.testm5", 1, "counter").getID();
int discrete_int = storage.getField("org.mozilla.testm5", 1, "discrete_int").getID();
int discrete_str = storage.getField("org.mozilla.testm5", 1, "discrete_str").getID();
int last_int = storage.getField("org.mozilla.testm5", 1, "last_int").getID();
int last_str = storage.getField("org.mozilla.testm5", 1, "last_str").getID();
int counted_str = storage.getField("org.mozilla.testm5", 1, "counted_str").getID();
int discrete_json = storage.getField("org.mozilla.testm5", 1, "discrete_json").getID();
storage.incrementDailyCount(env, day, counter, 2);
storage.incrementDailyCount(env, day, counter, 3);
storage.recordDailyLast(env, day, last_int, 2);
storage.recordDailyLast(env, day, last_str, "a");
storage.recordDailyLast(env, day, last_int, 3);
storage.recordDailyLast(env, day, last_str, "b");
storage.recordDailyDiscrete(env, day, discrete_str, "a");
storage.recordDailyDiscrete(env, day, discrete_str, "b");
storage.recordDailyDiscrete(env, day, discrete_int, 2);
storage.recordDailyDiscrete(env, day, discrete_int, 1);
storage.recordDailyDiscrete(env, day, discrete_int, 3);
storage.recordDailyDiscrete(env, day, counted_str, "aaa");
storage.recordDailyDiscrete(env, day, counted_str, "ccc");
storage.recordDailyDiscrete(env, day, counted_str, "bbb");
storage.recordDailyDiscrete(env, day, counted_str, "aaa");
JSONObject objA = new JSONObject();
objA.put("foo", "bar");
storage.recordDailyDiscrete(env, day, discrete_json, (JSONObject) null);
storage.recordDailyDiscrete(env, day, discrete_json, "null"); // Still works because JSON is a string internally.
storage.recordDailyDiscrete(env, day, discrete_json, objA);
JSONObject document = gen.generateDocument(0, HealthReportConstants.EARLIEST_LAST_PING, environment);
JSONObject today = document.getJSONObject("data").getJSONObject("days").getJSONObject(todayString);
assertEquals(1, today.length());
JSONObject measurement = today.getJSONObject(envHash).getJSONObject("org.mozilla.testm5");
assertEquals(1, measurement.getInt("_v"));
assertEquals(5, measurement.getInt("counter"));
assertEquals(3, measurement.getInt("last_int"));
assertEquals("b", measurement.getString("last_str"));
JSONArray discreteInts = measurement.getJSONArray("discrete_int");
JSONArray discreteStrs = measurement.getJSONArray("discrete_str");
assertEquals(3, discreteInts.length());
assertEquals(2, discreteStrs.length());
assertEquals("a", discreteStrs.get(0));
assertEquals("b", discreteStrs.get(1));
assertEquals(Long.valueOf(2), discreteInts.get(0));
assertEquals(Long.valueOf(1), discreteInts.get(1));
assertEquals(Long.valueOf(3), discreteInts.get(2));
JSONObject counted = measurement.getJSONObject("counted_str");
assertEquals(2, counted.getInt("aaa"));
assertEquals(1, counted.getInt("bbb"));
assertEquals(1, counted.getInt("ccc"));
assertFalse(counted.has("ddd"));
JSONArray discreteJSON = measurement.getJSONArray("discrete_json");
assertEquals(3, discreteJSON.length());
assertEquals(JSONObject.NULL, discreteJSON.get(0));
assertEquals(JSONObject.NULL, discreteJSON.get(1));
assertEquals("bar", discreteJSON.getJSONObject(2).getString("foo"));
}
@Override
protected String getCacheSuffix() {
return File.separator + "health-" + System.currentTimeMillis() + ".profile";
}
}

Просмотреть файл

@ -0,0 +1,254 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import org.mozilla.gecko.background.helpers.DBHelpers;
import org.mozilla.gecko.background.helpers.DBProviderTestCase;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.test.mock.MockContentResolver;
public class TestHealthReportProvider extends DBProviderTestCase<HealthReportProvider> {
protected static final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
public TestHealthReportProvider() {
super(HealthReportProvider.class, HealthReportProvider.HEALTH_AUTHORITY);
}
public TestHealthReportProvider(Class<HealthReportProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
@Override
protected String getCacheSuffix() {
return File.separator + "health-" + System.currentTimeMillis() + ".profile";
}
private Uri getCompleteUri(String rest) {
return Uri.parse("content://" + HealthReportProvider.HEALTH_AUTHORITY + rest +
(rest.indexOf('?') == -1 ? "?" : "&") +
"profilePath=" + Uri.encode(fakeProfileDirectory.getAbsolutePath()));
}
private void ensureCount(int expected, Uri uri) {
final MockContentResolver resolver = getMockContentResolver();
Cursor cursor = resolver.query(uri, null, null, null, null);
assertNotNull(cursor);
assertEquals(expected, cursor.getCount());
cursor.close();
}
private void ensureMeasurementCount(int expected) {
final Uri measurements = getCompleteUri("/measurements/");
ensureCount(expected, measurements);
}
private void ensureFieldCount(int expected) {
final Uri fields = getCompleteUri("/fields/");
ensureCount(expected, fields);
}
public void testNonExistentMeasurement() {
assertNotNull(getContext());
Uri u = getCompleteUri("/events/" + 0 + "/" + "testm" + "/" + 3 + "/" + "testf");
ContentValues v = new ContentValues();
v.put("value", 5);
ContentResolver r = getMockContentResolver();
assertNotNull(r);
try {
r.insert(u, v);
fail("Should throw.");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("No field with name testf"));
}
}
public void testEnsureMeasurements() {
ensureMeasurementCount(0);
final MockContentResolver resolver = getMockContentResolver();
// Note that we insert no fields. These are empty measurements.
ContentValues values = new ContentValues();
resolver.insert(getCompleteUri("/fields/testm1/1"), values);
ensureMeasurementCount(1);
resolver.insert(getCompleteUri("/fields/testm1/1"), values);
ensureMeasurementCount(1);
resolver.insert(getCompleteUri("/fields/testm1/3"), values);
ensureMeasurementCount(2);
resolver.insert(getCompleteUri("/fields/testm2/1"), values);
ensureMeasurementCount(3);
Cursor cursor = resolver.query(getCompleteUri("/measurements/"), null, null, null, null);
assertTrue(cursor.moveToFirst());
assertEquals("testm1", cursor.getString(1)); // 'id' is column 0.
assertEquals(1, cursor.getInt(2));
assertTrue(cursor.moveToNext());
assertEquals("testm1", cursor.getString(1));
assertEquals(3, cursor.getInt(2));
assertTrue(cursor.moveToNext());
assertEquals("testm2", cursor.getString(1));
assertEquals(1, cursor.getInt(2));
assertFalse(cursor.moveToNext());
cursor.close();
resolver.delete(getCompleteUri("/measurements/"), null, null);
}
/**
* Return true if the two times occur on the same UTC day.
*/
private static boolean sameDay(long start, long end) {
return Math.floor(start / MILLISECONDS_PER_DAY) ==
Math.floor(end / MILLISECONDS_PER_DAY);
}
private static int getDay(long time) {
return (int) Math.floor(time / MILLISECONDS_PER_DAY);
}
public void testRealData() {
ensureMeasurementCount(0);
long start = System.currentTimeMillis();
int day = getDay(start);
final MockContentResolver resolver = getMockContentResolver();
// Register a provider with four fields.
ContentValues values = new ContentValues();
values.put("counter1", 1);
values.put("counter2", 4);
values.put("last1", 7);
values.put("discrete1", 11);
resolver.insert(getCompleteUri("/fields/testm1/1"), values);
ensureMeasurementCount(1);
ensureFieldCount(4);
final Uri envURI = resolver.insert(getCompleteUri("/environments/"), getTestEnvContentValues());
String envHash = null;
Cursor envCursor = resolver.query(envURI, null, null, null, null);
try {
assertTrue(envCursor.moveToFirst());
envHash = envCursor.getString(1);
} finally {
envCursor.close();
}
final Uri eventURI = HealthReportUtils.getEventURI(envURI);
Uri discrete1 = eventURI.buildUpon().appendEncodedPath("testm1/1/discrete1").build();
Uri counter1 = eventURI.buildUpon().appendEncodedPath("testm1/1/counter1/counter").build();
Uri counter2 = eventURI.buildUpon().appendEncodedPath("testm1/1/counter2/counter").build();
Uri last1 = eventURI.buildUpon().appendEncodedPath("testm1/1/last1/last").build();
ContentValues discreteS = new ContentValues();
ContentValues discreteI = new ContentValues();
discreteS.put("value", "Some string");
discreteI.put("value", 9);
resolver.insert(discrete1, discreteS);
resolver.insert(discrete1, discreteI);
ContentValues counter = new ContentValues();
resolver.update(counter1, counter, null, null); // Defaults to 1.
resolver.update(counter2, counter, null, null); // Defaults to 1.
counter.put("value", 3);
resolver.update(counter2, counter, null, null); // Increment by 3.
// Interleaving.
discreteS.put("value", "Some other string");
discreteI.put("value", 3);
resolver.insert(discrete1, discreteS);
resolver.insert(discrete1, discreteI);
// Note that we explicitly do not support last-values transitioning between types.
ContentValues last = new ContentValues();
last.put("value", 123);
resolver.update(last1, last, null, null);
last.put("value", 245);
resolver.update(last1, last, null, null);
int expectedRows = 2 + 1 + 4; // Two counters, one last, four entries for discrete.
// Now let's see what comes up in the query!
// We'll do "named" first -- the results include strings.
Cursor cursor = resolver.query(getCompleteUri("/events/?time=" + start), null, null, null, null);
assertEquals(expectedRows, cursor.getCount());
assertTrue(cursor.moveToFirst());
// Let's be safe in case someone runs this test at midnight.
long end = System.currentTimeMillis();
if (!sameDay(start, end)) {
System.out.println("Aborting testAddData: spans midnight.");
cursor.close();
return;
}
// "date", "env", m, mv, f, f_flags, "value"
Object[][] expected = {
{day, envHash, "testm1", 1, "counter1", null, 1},
{day, envHash, "testm1", 1, "counter2", null, 4},
// Discrete values don't preserve order of insertion across types, but
// this actually isn't really permitted -- fields have a single type.
{day, envHash, "testm1", 1, "discrete1", null, 9},
{day, envHash, "testm1", 1, "discrete1", null, 3},
{day, envHash, "testm1", 1, "discrete1", null, "Some string"},
{day, envHash, "testm1", 1, "discrete1", null, "Some other string"},
{day, envHash, "testm1", 1, "last1", null, 245},
};
DBHelpers.assertCursorContains(expected, cursor);
cursor.close();
resolver.delete(getCompleteUri("/measurements/"), null, null);
ensureMeasurementCount(0);
ensureFieldCount(0);
}
private ContentValues getTestEnvContentValues() {
ContentValues v = new ContentValues();
v.put("profileCreation", 0);
v.put("cpuCount", 0);
v.put("memoryMB", 0);
v.put("isBlocklistEnabled", 0);
v.put("isTelemetryEnabled", 0);
v.put("extensionCount", 0);
v.put("pluginCount", 0);
v.put("themeCount", 0);
v.put("architecture", "");
v.put("sysName", "");
v.put("sysVersion", "");
v.put("vendor", "");
v.put("appName", "");
v.put("appID", "");
v.put("appVersion", "");
v.put("appBuildID", "");
v.put("platformVersion", "");
v.put("platformBuildID", "");
v.put("os", "");
v.put("xpcomabi", "");
v.put("updateChannel", "");
return v;
}
}

Просмотреть файл

@ -0,0 +1,172 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import org.mozilla.gecko.background.helpers.DBHelpers;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
public class TestHealthReportSQLiteOpenHelper extends FakeProfileTestCase {
private MockHealthReportSQLiteOpenHelper helper;
@Override
protected void setUp() throws Exception {
super.setUp();
helper = null;
}
@Override
protected void tearDown() throws Exception {
if (helper != null) {
helper.close();
helper = null;
}
super.tearDown();
}
private MockHealthReportSQLiteOpenHelper createHelper(String name) {
return new MockHealthReportSQLiteOpenHelper(context, fakeProfileDirectory, name);
}
private MockHealthReportSQLiteOpenHelper createHelper(String name, int version) {
return new MockHealthReportSQLiteOpenHelper(context, fakeProfileDirectory, name, version);
}
@Override
protected String getCacheSuffix() {
return File.separator + "testHealth";
}
public void testOpening() {
helper = createHelper("health.db");
SQLiteDatabase db = helper.getWritableDatabase();
assertTrue(db.isOpen());
db.beginTransaction();
db.setTransactionSuccessful();
db.endTransaction();
helper.close();
assertFalse(db.isOpen());
}
private void assertEmptyTable(SQLiteDatabase db, String table, String column) {
Cursor c = db.query(table, new String[] { column },
null, null, null, null, null);
assertNotNull(c);
try {
assertFalse(c.moveToFirst());
} finally {
c.close();
}
}
public void testInit() {
helper = createHelper("health-" + System.currentTimeMillis() + ".db");
SQLiteDatabase db = helper.getWritableDatabase();
assertTrue(db.isOpen());
db.beginTransaction();
try {
// DB starts empty with correct tables.
assertEmptyTable(db, "fields", "name");
assertEmptyTable(db, "measurements", "name");
assertEmptyTable(db, "events_textual", "field");
assertEmptyTable(db, "events_integer", "field");
assertEmptyTable(db, "events", "field");
// Throws for tables that don't exist.
try {
assertEmptyTable(db, "foobarbaz", "name");
} catch (SQLiteException e) {
// Expected.
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void testUpgradeDatabaseFrom4To5() throws Exception {
final String dbName = "health-4To5.db";
helper = createHelper(dbName, 4);
SQLiteDatabase db = helper.getWritableDatabase();
db.beginTransaction();
try {
db.execSQL("PRAGMA foreign_keys=OFF;");
// Despite being referenced, this addon should be deleted because it is NULL.
ContentValues v = new ContentValues();
v.put("body", (String) null);
final long orphanedAddonID = db.insert("addons", null, v);
v.put("body", "addon");
final long addonID = db.insert("addons", null, v);
// environments -> addons
v = new ContentValues();
v.put("hash", "orphanedEnv");
v.put("addonsID", orphanedAddonID);
final long orphanedEnvID = db.insert("environments", null, v);
v.put("hash", "env");
v.put("addonsID", addonID);
final long envID = db.insert("environments", null, v);
v = new ContentValues();
v.put("name", "measurement");
v.put("version", 1);
final long measurementID = db.insert("measurements", null, v);
// fields -> measurements
v = new ContentValues();
v.put("name", "orphanedField");
v.put("measurement", DBHelpers.getNonExistentID(db, "measurements"));
final long orphanedFieldID = db.insert("fields", null, v);
v.put("name", "field");
v.put("measurement", measurementID);
final long fieldID = db.insert("fields", null, v);
// events -> environments, fields
final String[] eventTables = {"events_integer", "events_textual"};
for (String table : eventTables) {
v = new ContentValues();
v.put("env", envID);
v.put("field", fieldID);
db.insert(table, null, v);
v.put("env", orphanedEnvID);
v.put("field", fieldID);
db.insert(table, null, v);
v.put("env", envID);
v.put("field", orphanedFieldID);
db.insert(table, null, v);
v.put("env", orphanedEnvID);
v.put("field", orphanedFieldID);
db.insert(table, null, v);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
helper.close();
}
// Upgrade.
helper = createHelper(dbName, 5);
// Despite only reading from it, open a writable database so we can better replicate what
// might happen in production (most notably, this should enable foreign keys).
db = helper.getWritableDatabase();
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
assertEquals(1, DBHelpers.getRowCount(db, "measurements"));
assertEquals(1, DBHelpers.getRowCount(db, "fields"));
assertEquals(1, DBHelpers.getRowCount(db, "events_integer"));
assertEquals(1, DBHelpers.getRowCount(db, "events_textual"));
}
}

Просмотреть файл

@ -0,0 +1,185 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
public class TestProfileInformationCache extends FakeProfileTestCase {
public final void testInitState() throws IOException {
MockProfileInformationCache cache = new MockProfileInformationCache(this.fakeProfileDirectory.getAbsolutePath());
assertFalse(cache.isInitialized());
assertFalse(cache.needsWrite());
try {
cache.isBlocklistEnabled();
fail("Should throw fetching isBlocklistEnabled.");
} catch (IllegalStateException e) {
// Great!
}
cache.beginInitialization();
assertFalse(cache.isInitialized());
assertTrue(cache.needsWrite());
try {
cache.isBlocklistEnabled();
fail("Should throw fetching isBlocklistEnabled.");
} catch (IllegalStateException e) {
// Great!
}
cache.completeInitialization();
assertTrue(cache.isInitialized());
assertFalse(cache.needsWrite());
}
public final MockProfileInformationCache makeCache(final String suffix) {
File subdir = new File(this.fakeProfileDirectory.getAbsolutePath() + File.separator + suffix);
subdir.mkdir();
return new MockProfileInformationCache(subdir.getAbsolutePath());
}
public final void testPersisting() throws IOException {
MockProfileInformationCache cache = makeCache("testPersisting");
// We start with no file.
assertFalse(cache.getFile().exists());
// Partially populate. Note that this doesn't happen in live code, but
// apparently we can end up with null add-ons JSON on disk, so this
// reproduces that scenario.
cache.beginInitialization();
cache.setBlocklistEnabled(true);
cache.setTelemetryEnabled(true);
cache.setProfileCreationTime(1234L);
cache.completeInitialization();
assertTrue(cache.getFile().exists());
// But reading this from disk won't work, because we were only partially
// initialized. We want to start over.
cache = makeCache("testPersisting");
assertFalse(cache.isInitialized());
assertFalse(cache.restoreUnlessInitialized());
assertFalse(cache.isInitialized());
// Now fully populate, and try again...
cache.beginInitialization();
cache.setBlocklistEnabled(true);
cache.setTelemetryEnabled(true);
cache.setProfileCreationTime(1234L);
cache.setJSONForAddons(new JSONObject());
cache.completeInitialization();
assertTrue(cache.getFile().exists());
// ... and this time we succeed.
cache = makeCache("testPersisting");
assertFalse(cache.isInitialized());
assertTrue(cache.restoreUnlessInitialized());
assertTrue(cache.isInitialized());
assertTrue(cache.isBlocklistEnabled());
assertTrue(cache.isTelemetryEnabled());
assertEquals(1234L, cache.getProfileCreationTime());
// Mutate.
cache.beginInitialization();
assertFalse(cache.isInitialized());
cache.setBlocklistEnabled(false);
cache.setProfileCreationTime(2345L);
cache.completeInitialization();
assertTrue(cache.isInitialized());
cache = makeCache("testPersisting");
assertFalse(cache.isInitialized());
assertTrue(cache.restoreUnlessInitialized());
assertTrue(cache.isInitialized());
assertFalse(cache.isBlocklistEnabled());
assertTrue(cache.isTelemetryEnabled());
assertEquals(2345L, cache.getProfileCreationTime());
}
public final void testVersioning() throws JSONException, IOException {
MockProfileInformationCache cache = makeCache("testVersioning");
final int currentVersion = ProfileInformationCache.FORMAT_VERSION;
final JSONObject json = cache.toJSON();
assertEquals(currentVersion, json.getInt("version"));
// Initialize enough that we can re-load it.
cache.beginInitialization();
cache.setJSONForAddons(new JSONObject());
cache.completeInitialization();
cache.writeJSON(json);
assertTrue(cache.restoreUnlessInitialized());
cache.beginInitialization(); // So that we'll need to read again.
json.put("version", currentVersion + 1);
cache.writeJSON(json);
// We can't restore a future version.
assertFalse(cache.restoreUnlessInitialized());
}
public void testRestoreInvalidJSON() throws Exception {
final MockProfileInformationCache cache = makeCache("invalid");
final JSONObject invalidJSON = new JSONObject();
invalidJSON.put("blocklist", true);
invalidJSON.put("telemetry", false);
invalidJSON.put("profileCreated", 1234567L);
cache.writeJSON(invalidJSON);
assertFalse(cache.restoreUnlessInitialized());
}
private JSONObject getValidCacheJSON() throws Exception {
final JSONObject json = new JSONObject();
json.put("blocklist", true);
json.put("telemetry", false);
json.put("profileCreated", 1234567L);
json.put("addons", new JSONObject());
json.put("version", ProfileInformationCache.FORMAT_VERSION);
return json;
}
public void testRestoreImplicitV1() throws Exception {
assertTrue(ProfileInformationCache.FORMAT_VERSION > 1);
final MockProfileInformationCache cache = makeCache("implicitV1");
final JSONObject json = getValidCacheJSON();
json.remove("version");
cache.writeJSON(json);
// Can't restore v1 (which is implicitly set) since it is not the current version.
assertFalse(cache.restoreUnlessInitialized());
}
public void testRestoreOldVersion() throws Exception {
final int oldVersion = 1;
assertTrue(ProfileInformationCache.FORMAT_VERSION > oldVersion);
final MockProfileInformationCache cache = makeCache("oldVersion");
final JSONObject json = getValidCacheJSON();
json.put("version", oldVersion);
cache.writeJSON(json);
assertFalse(cache.restoreUnlessInitialized());
}
public void testRestoreCurrentVersion() throws Exception {
final MockProfileInformationCache cache = makeCache("currentVersion");
final JSONObject json = getValidCacheJSON();
cache.writeJSON(json);
cache.beginInitialization();
cache.setTelemetryEnabled(true);
cache.completeInitialization();
assertEquals(ProfileInformationCache.FORMAT_VERSION, cache.readJSON().getInt("version"));
}
@Override
protected String getCacheSuffix() {
return System.currentTimeMillis() + Math.random() + ".foo";
}
}

Просмотреть файл

@ -0,0 +1,113 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.prune;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
import android.test.mock.MockContext;
public class TestHealthReportPruneService
extends BackgroundServiceTestCase<TestHealthReportPruneService.MockHealthReportPruneService> {
public static class MockHealthReportPruneService extends HealthReportPruneService {
protected MockPrunePolicy prunePolicy;
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
@Override
public boolean isIntentValid(final Intent intent) {
return super.isIntentValid(intent);
}
@Override
public PrunePolicy getPrunePolicy(final String profilePath) {
final PrunePolicyStorage storage = new PrunePolicyDatabaseStorage(new MockContext(), profilePath);
prunePolicy = new MockPrunePolicy(storage, getSharedPreferences());
return prunePolicy;
}
public boolean wasTickCalled() {
if (prunePolicy == null) {
return false;
}
return prunePolicy.wasTickCalled();
}
}
// TODO: This is a spy - perhaps we should be using a framework for this.
public static class MockPrunePolicy extends PrunePolicy {
private boolean wasTickCalled;
public MockPrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPreferences) {
super(storage, sharedPreferences);
wasTickCalled = false;
}
@Override
public void tick(final long time) {
wasTickCalled = true;
}
public boolean wasTickCalled() {
return wasTickCalled;
}
}
public TestHealthReportPruneService() {
super(MockHealthReportPruneService.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
}
public void testIsIntentValid() throws Exception {
// No profilePath or profileName.
startService(intent);
await();
assertFalse(getService().wasTickCalled());
barrier.reset();
// No profilePath.
intent.putExtra("profileName", "profileName");
startService(intent);
await();
assertFalse(getService().wasTickCalled());
barrier.reset();
// No profileName.
intent.putExtra("profilePath", "profilePath")
.removeExtra("profileName");
startService(intent);
await();
assertFalse(getService().wasTickCalled());
barrier.reset();
intent.putExtra("profileName", "profileName");
startService(intent);
await();
assertTrue(getService().wasTickCalled());
}
}

Просмотреть файл

@ -0,0 +1,114 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.prune;
import java.io.File;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.content.Context;
public class TestPrunePolicyDatabaseStorage extends FakeProfileTestCase {
public static class MockPrunePolicyDatabaseStorage extends PrunePolicyDatabaseStorage {
public final MockHealthReportDatabaseStorage storage;
public int currentEnvironmentID;
public MockPrunePolicyDatabaseStorage(final Context context, final String profilePath) {
super(context, profilePath);
storage = new MockHealthReportDatabaseStorage(context, new File(profilePath));
currentEnvironmentID = -1;
}
@Override
public HealthReportDatabaseStorage getStorage() {
return storage;
}
@Override
public int getCurrentEnvironmentID() {
return currentEnvironmentID;
}
}
public static class MockHealthReportDatabaseStorage extends HealthReportDatabaseStorage {
private boolean wasPruneEventsCalled = false;
private boolean wasPruneEnvironmentsCalled = false;
private boolean wasDeleteDataBeforeCalled = false;
private boolean wasDisableAutoVacuumingCalled = false;
private boolean wasVacuumCalled = false;
public MockHealthReportDatabaseStorage(final Context context, final File file) {
super(context, file);
}
// We use spies here to avoid doing expensive DB operations (which are tested elsewhere).
@Override
public void pruneEvents(final int count) {
wasPruneEventsCalled = true;
}
@Override
public void pruneEnvironments(final int count) {
wasPruneEnvironmentsCalled = true;
}
@Override
public int deleteDataBefore(final long time, final int curEnv) {
wasDeleteDataBeforeCalled = true;
return -1;
}
@Override
public void disableAutoVacuuming() {
wasDisableAutoVacuumingCalled = true;
}
@Override
public void vacuum() {
wasVacuumCalled = true;
}
}
public MockPrunePolicyDatabaseStorage policyStorage;
@Override
public void setUp() throws Exception {
super.setUp();
policyStorage = new MockPrunePolicyDatabaseStorage(context, "profilePath");
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@Override
protected String getCacheSuffix() {
return "health-" + System.currentTimeMillis() + ".profile";
}
public void testPruneEvents() throws Exception {
policyStorage.pruneEvents(0);
assertTrue(policyStorage.storage.wasPruneEventsCalled);
}
public void testPruneEnvironments() throws Exception {
policyStorage.pruneEnvironments(0);
assertTrue(policyStorage.storage.wasPruneEnvironmentsCalled);
}
public void testDeleteDataBefore() throws Exception {
policyStorage.deleteDataBefore(-1);
assertTrue(policyStorage.storage.wasDeleteDataBeforeCalled);
}
public void testCleanup() throws Exception {
policyStorage.cleanup();
assertTrue(policyStorage.storage.wasDisableAutoVacuumingCalled);
assertTrue(policyStorage.storage.wasVacuumCalled);
}
}

Просмотреть файл

@ -0,0 +1,96 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
public class TestHealthReportUploadService
extends BackgroundServiceTestCase<TestHealthReportUploadService.MockHealthReportUploadService> {
public static class MockHealthReportUploadService extends HealthReportUploadService {
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
}
public TestHealthReportUploadService() {
super(MockHealthReportUploadService.class);
}
protected boolean isFirstRunSet() throws Exception {
return getSharedPreferences().contains(HealthReportConstants.PREF_FIRST_RUN);
}
@Override
public void setUp() throws Exception {
super.setUp();
// First run state is used for comparative testing.
assertFalse(isFirstRunSet());
}
public void testFailedFirstRun() throws Exception {
// Missing "uploadEnabled".
intent.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertFalse(isFirstRunSet());
barrier.reset();
// Missing "profilePath".
intent.putExtra("uploadEnabled", true)
.removeExtra("profilePath");
startService(intent);
await();
assertFalse(isFirstRunSet());
barrier.reset();
// Missing "profileName".
intent.putExtra("profilePath", "profilePath")
.removeExtra("profileName");
startService(intent);
assertFalse(isFirstRunSet());
await();
assertFalse(isFirstRunSet());
}
public void testUploadDisabled() throws Exception {
intent.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath")
.putExtra("uploadEnabled", false);
startService(intent);
await();
assertFalse(isFirstRunSet());
}
public void testSuccessfulFirstRun() throws Exception {
intent.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath")
.putExtra("uploadEnabled", true);
startService(intent);
await();
assertTrue(isFirstRunSet());
}
}

Просмотреть файл

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.helpers;
import junit.framework.AssertionFailedError;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import android.app.Activity;
import android.content.Context;
import android.test.ActivityInstrumentationTestCase2;
/**
* AndroidSyncTestCase provides helper methods for testing.
*/
public class AndroidSyncTestCase extends ActivityInstrumentationTestCase2<Activity> {
protected static String LOG_TAG = "AndroidSyncTestCase";
public AndroidSyncTestCase() {
super(Activity.class);
WaitHelper.resetTestWaiter();
}
public Context getApplicationContext() {
return this.getInstrumentation().getTargetContext().getApplicationContext();
}
public static void performWait(Runnable runnable) {
try {
WaitHelper.getTestWaiter().performWait(runnable);
} catch (WaitHelper.InnerError e) {
AssertionFailedError inner = new AssertionFailedError("Caught error in performWait");
inner.initCause(e.innerError);
throw inner;
}
}
public static void performNotify() {
WaitHelper.getTestWaiter().performNotify();
}
public static void performNotify(Throwable e) {
WaitHelper.getTestWaiter().performNotify(e);
}
public static void performNotify(String reason, Throwable e) {
AssertionFailedError er = new AssertionFailedError(reason + ": " + e.getMessage());
er.initCause(e);
WaitHelper.getTestWaiter().performNotify(er);
}
}

Просмотреть файл

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.helpers;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.test.ServiceTestCase;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.UUID;
import org.mozilla.gecko.background.common.GlobalConstants;
/**
* An abstract test class for testing background services. Since we have to wait for background
* services to finish before asserting the changed state, this class provides much of the
* functionality to do this. Extending classes need still need to implement some of the components -
* see {@link TestHealthReportBroadcastService} for an example.
*/
public abstract class BackgroundServiceTestCase<T extends Service> extends ServiceTestCase<T> {
private static final String SHARED_PREFS_PREFIX = "BackgroundServiceTestCase-";
// Ideally, this would not be static so multiple test classes can be run in parallel. However,
// mServiceClass can only retrieve this reference statically because mServiceClass cannot get a
// reference to the Test* class as ServiceTestCase instantiates it via reflection and we can't
// pass it as a constructor arg.
protected static String sharedPrefsName;
private final Class<T> mServiceClass;
protected static CyclicBarrier barrier;
protected Intent intent;
public BackgroundServiceTestCase(Class<T> serviceClass) {
super(serviceClass);
mServiceClass = serviceClass;
}
@Override
public void setUp() throws Exception {
barrier = new CyclicBarrier(2);
intent = new Intent(getContext(), mServiceClass);
sharedPrefsName = SHARED_PREFS_PREFIX + mServiceClass.getName() + "-" + UUID.randomUUID();
}
@Override
public void tearDown() throws Exception {
barrier = null;
intent = null;
clearSharedPrefs(); // Not necessary but reduces file system cruft.
}
protected SharedPreferences getSharedPreferences() {
return getContext().getSharedPreferences(sharedPrefsName,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
protected void clearSharedPrefs() {
getSharedPreferences().edit()
.clear()
.commit();
}
protected void await() {
try {
barrier.await();
} catch (InterruptedException e) {
fail("Test runner thread should not be interrupted.");
} catch (BrokenBarrierException e) {
fail("Background services should not timeout or be interrupted");
}
}
protected void cancelAlarm(Intent intent) {
final AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
final PendingIntent pi = PendingIntent.getService(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
am.cancel(pi);
pi.cancel();
}
protected boolean isServiceAlarmSet(Intent intent) {
return PendingIntent.getService(getContext(), 0, intent, PendingIntent.FLAG_NO_CREATE) != null;
}
}

Просмотреть файл

@ -0,0 +1,84 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.helpers;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import junit.framework.Assert;
public class DBHelpers {
/*
* Works for strings and int-ish values.
*/
public static void assertCursorContains(Object[][] expected, Cursor actual) {
Assert.assertEquals(expected.length, actual.getCount());
int i = 0, j = 0;
Object[] row;
do {
row = expected[i];
for (j = 0; j < row.length; ++j) {
Object atIndex = row[j];
if (atIndex == null) {
continue;
}
if (atIndex instanceof String) {
Assert.assertEquals(atIndex, actual.getString(j));
} else {
Assert.assertEquals(atIndex, actual.getInt(j));
}
}
++i;
} while (actual.moveToPosition(i));
}
public static int getRowCount(SQLiteDatabase db, String table) {
return getRowCount(db, table, null, null);
}
public static int getRowCount(SQLiteDatabase db, String table, String selection, String[] selectionArgs) {
final Cursor c = db.query(table, null, selection, selectionArgs, null, null, null);
try {
return c.getCount();
} finally {
c.close();
}
}
/**
* Returns an ID that is non-existent in the given sqlite table. Assumes that a column named
* "id" exists.
*/
public static int getNonExistentID(SQLiteDatabase db, String table) {
// XXX: We should use selectionArgs to concatenate table, but sqlite throws a syntax error on
// "?" because it wants to ensure id is a valid column in table.
final Cursor c = db.rawQuery("SELECT MAX(id) + 1 FROM " + table, null);
try {
if (!c.moveToNext()) {
return 0;
}
return c.getInt(0);
} finally {
c.close();
}
}
/**
* Returns an ID that exists in the given sqlite table. Assumes that a column named * "id"
* exists.
*/
public static long getExistentID(SQLiteDatabase db, String table) {
final Cursor c = db.query(table, new String[] {"id"}, null, null, null, null, null, "1");
try {
if (!c.moveToNext()) {
throw new IllegalStateException("Given table does not contain any entries.");
}
return c.getInt(0);
} finally {
c.close();
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.helpers;
import java.io.File;
import android.content.ContentProvider;
import android.content.Context;
import android.test.AndroidTestCase;
import android.test.mock.MockContentResolver;
/**
* Because ProviderTestCase2 is unable to handle custom DB paths.
*/
public abstract class DBProviderTestCase<T extends ContentProvider> extends
AndroidTestCase {
Class<T> providerClass;
String providerAuthority;
protected File fakeProfileDirectory;
private MockContentResolver resolver;
private T provider;
public DBProviderTestCase(Class<T> providerClass, String providerAuthority) {
this.providerClass = providerClass;
this.providerAuthority = providerAuthority;
}
public T getProvider() {
return provider;
}
public MockContentResolver getMockContentResolver() {
return resolver;
}
protected abstract String getCacheSuffix();
@Override
protected void setUp() throws Exception {
super.setUp();
File cache = getContext().getCacheDir();
fakeProfileDirectory = new File(cache.getAbsolutePath() + getCacheSuffix());
System.out.println("Test: Creating profile directory " + fakeProfileDirectory.getAbsolutePath());
if (!fakeProfileDirectory.mkdir()) {
throw new IllegalStateException("Could not create temporary directory.");
}
final Context context = getContext();
assertNotNull(context);
resolver = new MockContentResolver();
provider = providerClass.newInstance();
provider.attachInfo(context, null);
assertNotNull(provider);
resolver.addProvider(providerAuthority, getProvider());
}
@Override
protected void tearDown() throws Exception {
// We don't check return values.
System.out.println("Test: Cleaning up " + fakeProfileDirectory.getAbsolutePath());
for (File child : fakeProfileDirectory.listFiles()) {
child.delete();
}
fakeProfileDirectory.delete();
super.tearDown();
}
}

Просмотреть файл

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.helpers;
import java.io.File;
import android.app.Activity;
import android.content.Context;
import android.test.ActivityInstrumentationTestCase2;
public abstract class FakeProfileTestCase extends ActivityInstrumentationTestCase2<Activity> {
protected Context context;
protected File fakeProfileDirectory;
public FakeProfileTestCase() {
super(Activity.class);
}
protected abstract String getCacheSuffix();
@Override
protected void setUp() throws Exception {
super.setUp();
context = getInstrumentation().getTargetContext();
File cache = context.getCacheDir();
fakeProfileDirectory = new File(cache.getAbsolutePath() + getCacheSuffix());
if (!fakeProfileDirectory.mkdir()) {
throw new IllegalStateException("Could not create temporary directory.");
}
}
@Override
protected void tearDown() throws Exception {
// We don't check return values.
for (File child : fakeProfileDirectory.listFiles()) {
child.delete();
}
fakeProfileDirectory.delete();
super.tearDown();
}
}

Просмотреть файл

@ -0,0 +1,248 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.AccountPickler;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
public class TestAccountPickler extends AndroidSyncTestCase {
public static final String TEST_FILENAME = "test.json";
public static final String TEST_ACCOUNTTYPE = SyncConstants.ACCOUNTTYPE_SYNC;
// Test account names must start with TEST_USERNAME in order to be recognized
// as test accounts and deleted in tearDown.
public static final String TEST_USERNAME = "testAccount@mozilla.com";
public static final String TEST_USERNAME2 = TEST_USERNAME + "2";
public static final String TEST_SYNCKEY = "testSyncKey";
public static final String TEST_PASSWORD = "testPassword";
public static final String TEST_SERVER_URL = "test.server.url/";
public static final String TEST_CLUSTER_URL = "test.cluster.url/";
public static final String TEST_CLIENT_NAME = "testClientName";
public static final String TEST_CLIENT_GUID = "testClientGuid";
public static final String TEST_PRODUCT = GlobalConstants.BROWSER_INTENT_PACKAGE;
public static final String TEST_PROFILE = "default";
public static final long TEST_VERSION = SyncConfiguration.CURRENT_PREFS_VERSION;
protected SyncAccountParameters params;
protected Context context;
protected AccountManager accountManager;
protected int numAccounts;
public void setUp() {
context = getApplicationContext();
accountManager = AccountManager.get(context);
params = new SyncAccountParameters(context, accountManager,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVER_URL,
TEST_CLUSTER_URL, TEST_CLIENT_NAME, TEST_CLIENT_GUID);
numAccounts = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
}
public static List<Account> getTestAccounts(final AccountManager accountManager) {
final List<Account> testAccounts = new ArrayList<Account>();
final Account[] accounts = accountManager.getAccountsByType(TEST_ACCOUNTTYPE);
for (Account account : accounts) {
if (account.name.startsWith(TEST_USERNAME)) {
testAccounts.add(account);
}
}
return testAccounts;
}
public void deleteTestAccounts() {
for (Account account : getTestAccounts(accountManager)) {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
public void tearDown() {
deleteTestAccounts();
assertEquals(numAccounts, accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length);
}
public static void assertFileNotPresent(final Context context, final String filename) throws Exception {
// Verify file is not present.
FileInputStream fis = null;
try {
fis = context.openFileInput(TEST_FILENAME);
fail("Should get FileNotFoundException.");
} catch (FileNotFoundException e) {
// Do nothing; file should not exist.
} finally {
if (fis != null) {
fis.close();
}
}
}
public void testPersist() throws Exception {
context.deleteFile(TEST_FILENAME);
assertFileNotPresent(context, TEST_FILENAME);
AccountPickler.pickle(context, TEST_FILENAME, params, true);
final String s = Utils.readFile(context, TEST_FILENAME);
assertNotNull(s);
final ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(s);
assertEquals(TEST_USERNAME, o.getString(Constants.JSON_KEY_ACCOUNT));
assertEquals(TEST_PASSWORD, o.getString(Constants.JSON_KEY_PASSWORD));
assertEquals(TEST_SERVER_URL, o.getString(Constants.JSON_KEY_SERVER));
assertEquals(TEST_SYNCKEY, o.getString(Constants.JSON_KEY_SYNCKEY));
assertEquals(TEST_CLUSTER_URL, o.getString(Constants.JSON_KEY_CLUSTER));
assertEquals(TEST_CLIENT_NAME, o.getString(Constants.JSON_KEY_CLIENT_NAME));
assertEquals(TEST_CLIENT_GUID, o.getString(Constants.JSON_KEY_CLIENT_GUID));
assertEquals(Boolean.valueOf(true), o.get(Constants.JSON_KEY_SYNC_AUTOMATICALLY));
assertEquals(Long.valueOf(AccountPickler.VERSION), o.getLong(Constants.JSON_KEY_VERSION));
assertTrue(o.containsKey(Constants.JSON_KEY_TIMESTAMP));
}
public void testDeletePickle() throws Exception {
AccountPickler.pickle(context, TEST_FILENAME, params, false);
// Verify file is present.
final String s = Utils.readFile(context, TEST_FILENAME);
assertNotNull(s);
assertTrue(s.length() > 0);
AccountPickler.deletePickle(context, TEST_FILENAME);
assertFileNotPresent(context, TEST_FILENAME);
}
public Account deleteAccountsAndUnpickle(final Context context, final AccountManager accountManager, final String filename) {
deleteTestAccounts();
assertEquals(0, getTestAccounts(accountManager).size());
return AccountPickler.unpickle(context, filename);
}
public void testUnpickleSuccess() throws Exception {
AccountPickler.pickle(context, TEST_FILENAME, params, true);
// Make sure we have no accounts hanging around.
final Account account = deleteAccountsAndUnpickle(context, accountManager, TEST_FILENAME);
assertNotNull(account);
try {
assertEquals(1, getTestAccounts(accountManager).size());
assertTrue(ContentResolver.getSyncAutomatically(account, BrowserContract.AUTHORITY));
assertEquals(account.name, TEST_USERNAME);
// Verify Account parameters are in place. Not testing clusterURL since it's stored in
// shared prefs and it's less critical.
final String password = accountManager.getPassword(account);
final String serverURL = accountManager.getUserData(account, Constants.OPTION_SERVER);
final String syncKey = accountManager.getUserData(account, Constants.OPTION_SYNCKEY);
assertEquals(TEST_PASSWORD, password);
assertEquals(TEST_SERVER_URL, serverURL);
assertEquals(TEST_SYNCKEY, syncKey);
// Verify shared prefs parameters are in place.
final SharedPreferences prefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME, TEST_SERVER_URL, TEST_PROFILE, TEST_VERSION);
final String clusterURL = prefs.getString(SyncConfiguration.PREF_CLUSTER_URL, null);
final String clientName = prefs.getString(SyncConfiguration.PREF_CLIENT_NAME, null);
final String clientGuid = prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
assertEquals(TEST_CLUSTER_URL, clusterURL);
assertEquals(TEST_CLIENT_NAME, clientName);
assertEquals(TEST_CLIENT_GUID, clientGuid);
} finally {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
public void testUnpickleNoAutomatic() throws Exception {
AccountPickler.pickle(context, TEST_FILENAME, params, false);
// Make sure we have no accounts hanging around.
final Account account = deleteAccountsAndUnpickle(context, accountManager, TEST_FILENAME);
assertNotNull(account);
try {
assertEquals(1, getTestAccounts(accountManager).size());
assertFalse(ContentResolver.getSyncAutomatically(account, BrowserContract.AUTHORITY));
} finally {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
public void testUnpickleNoFile() {
// Just in case file is hanging around.
context.deleteFile(TEST_FILENAME);
final Account account = deleteAccountsAndUnpickle(context, accountManager, TEST_FILENAME);
assertNull(account);
}
public void testUnpickleIncompleteUserData() throws Exception {
final FileOutputStream fos = context.openFileOutput(TEST_FILENAME, Context.MODE_PRIVATE);
final PrintStream ps = (new PrintStream(fos));
ps.print("{}"); // Valid JSON, just missing everything.
ps.close();
fos.close();
final Account account = deleteAccountsAndUnpickle(context, accountManager, TEST_FILENAME);
assertNull(account);
}
public void testUnpickleMalformedFile() throws Exception {
final FileOutputStream fos = context.openFileOutput(TEST_FILENAME, Context.MODE_PRIVATE);
final PrintStream ps = (new PrintStream(fos));
ps.print("{1:!}"); // Not valid JSON.
ps.close();
fos.close();
final Account account = deleteAccountsAndUnpickle(context, accountManager, TEST_FILENAME);
assertNull(account);
}
public void testUnpickleAccountAlreadyExists() {
AccountPickler.pickle(context, TEST_FILENAME, params, false);
// Make sure we have no test accounts hanging around.
final Account account = deleteAccountsAndUnpickle(context, accountManager, TEST_FILENAME);
assertNotNull(account);
assertEquals(TEST_USERNAME, account.name);
// Now replace file with new username.
params = new SyncAccountParameters(context, accountManager,
TEST_USERNAME2, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVER_URL, null, TEST_CLIENT_NAME, TEST_CLIENT_GUID);
AccountPickler.pickle(context, TEST_FILENAME, params, false);
// Checking if sync accounts exist could try to unpickle. That unpickle
// would load an account with a different username, so we can check that
// nothing was unpickled by verifying that the username has not changed.
assertTrue(SyncAccounts.syncAccountsExist(context));
for (Account a : getTestAccounts(accountManager)) {
assertEquals(TEST_USERNAME, a.name);
}
}
}

Просмотреть файл

@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import org.json.simple.JSONArray;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
import android.content.Context;
public class TestClientsStage extends AndroidSyncTestCase {
private static final String TEST_USERNAME = "johndoe";
private static final String TEST_PASSWORD = "password";
private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
public void setUp() {
ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext());
db.wipeDB();
db.close();
}
public void testWipeClearsClients() throws Exception {
// Wiping clients is equivalent to a reset and dropping all local stored client records.
// Resetting is defined as being the same as for other engines -- discard local
// and remote timestamps, tracked failed records, and tracked records to fetch.
final Context context = getApplicationContext();
final ClientsDatabaseAccessor dataAccessor = new ClientsDatabaseAccessor(context);
final GlobalSessionCallback callback = new DefaultGlobalSessionCallback();
final ClientsDataDelegate delegate = new MockClientsDataDelegate();
final GlobalSession session = new GlobalSession(
SyncConfiguration.DEFAULT_USER_API,
null,
TEST_USERNAME, TEST_PASSWORD, null,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
callback, context, null, delegate);
SyncClientsEngineStage stage = new SyncClientsEngineStage() {
@Override
public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
if (db == null) {
db = dataAccessor;
}
return db;
}
};
String guid = "clientabcdef";
long lastModified = System.currentTimeMillis();
ClientRecord record = new ClientRecord(guid, "clients", lastModified , false);
record.name = "John's Phone";
record.type = "mobile";
record.commands = new JSONArray();
dataAccessor.store(record);
assertEquals(1, dataAccessor.clientsCount());
stage.wipeLocal(session);
try {
assertEquals(0, dataAccessor.clientsCount());
assertEquals(0L, session.config.getPersistedServerClientRecordTimestamp());
assertEquals(0, session.getClientsDelegate().getClientsCount());
} finally {
dataAccessor.close();
}
}
}

Просмотреть файл

@ -0,0 +1,284 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.util.Map;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.ConfigurationMigrator;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
public class TestConfigurationMigrator extends AndroidSyncTestCase {
/**
* A migrator that makes public certain protected static functions for testing.
*/
protected static class PublicMigrator extends ConfigurationMigrator {
public static int upgradeGlobals0to1(final SharedPreferences from, final SharedPreferences to) throws Exception {
return ConfigurationMigrator.upgradeGlobals0to1(from, to);
}
public static int downgradeGlobals1to0(final SharedPreferences from, final SharedPreferences to) throws Exception {
return ConfigurationMigrator.downgradeGlobals1to0(from, to);
}
public static int upgradeShared0to1(final SharedPreferences from, final SharedPreferences to) {
return ConfigurationMigrator.upgradeShared0to1(from, to);
}
public static int downgradeShared1to0(final SharedPreferences from, final SharedPreferences to) {
return ConfigurationMigrator.downgradeShared1to0(from, to);
}
public static int upgradeAndroidAccount0to1(final AccountManager accountManager, final Account account, final SharedPreferences to) throws Exception {
return ConfigurationMigrator.upgradeAndroidAccount0to1(accountManager, account, to);
}
public static int downgradeAndroidAccount1to0(final SharedPreferences from, final AccountManager accountManager, final Account account) throws Exception {
return ConfigurationMigrator.downgradeAndroidAccount1to0(from, accountManager, account);
}
};
public static final String TEST_USERNAME = "test@mozilla.com";
public static final String TEST_SYNCKEY = "testSyncKey";
public static final String TEST_PASSWORD = "testPassword";
public static final String TEST_SERVERURL = null;
public static final String TEST_PROFILE = "default";
public static final String TEST_PRODUCT = GlobalConstants.BROWSER_INTENT_PACKAGE;
public static final String TEST_GUID = "testGuid";
public static final String TEST_CLIENT_NAME = "test's Nightly on test";
public static final long TEST_NUM_CLIENTS = 2;
protected static void putJSON(final Editor editor, final String name, final String JSON) throws Exception {
editor.putString(name, ExtendedJSONObject.parseJSONObject(JSON).toJSONString());
}
/**
* Write a complete set of unversioned account prefs suitable for testing forward migration.
* @throws Exception
*/
public void populateAccountSharedPrefs(final SharedPreferences to) throws Exception {
final Editor editor = to.edit();
putJSON(editor, "forms.remote", "{\"timestamp\":1340402010180}");
putJSON(editor, "forms.local", "{\"timestamp\":1340402018565}");
editor.putString("forms.syncID", "JKAkk-wUEUpX");
putJSON(editor, "tabs.remote", "{\"timestamp\":1340401964300}");
putJSON(editor, "tabs.local", "{\"timestamp\":1340401970533}");
editor.putString("tabs.syncID", "604bXkw7dnUq");
putJSON(editor, "passwords.remote", "{\"timestamp\":1340401965150}");
putJSON(editor, "passwords.local", "{\"timestamp\":1340402005243}");
editor.putString("passwords.syncID", "VkTH0QiVj6dD");
putJSON(editor, "history.remote", "{\"timestamp\":1340402003640}");
putJSON(editor, "history.local", "{\"timestamp\":1340402015381}");
editor.putString("history.syncID", "fs1241n-JyWh");
putJSON(editor, "bookmarks.remote", "{\"timestamp\":1340402003370}");
putJSON(editor, "bookmarks.local", "{\"timestamp\":1340402008397}");
editor.putString("bookmarks.syncID", "P8gG8ERuJ4H1");
editor.putLong("metaGlobalLastModified", 1340401961960L);
putJSON(editor, "metaGlobalServerResponseBody", "{\"ttl\":31536000,\"id\":\"global\",\"payload\":\"{\\\"storageVersion\\\":5,\\\"syncID\\\":\\\"Z2RopSDg-0bE\\\",\\\"engines\\\":{\\\"history\\\":{\\\"syncID\\\":\\\"fs1241n-JyWh\\\",\\\"version\\\":1},\\\"bookmarks\\\":{\\\"syncID\\\":\\\"P8gG8ERuJ4H1\\\",\\\"version\\\":2},\\\"passwords\\\":{\\\"syncID\\\":\\\"VkTH0QiVj6dD\\\",\\\"version\\\":1},\\\"prefs\\\":{\\\"syncID\\\":\\\"4lESgyoYPXYI\\\",\\\"version\\\":2},\\\"addons\\\":{\\\"syncID\\\":\\\"yCkJKkH-okoS\\\",\\\"version\\\":1},\\\"forms\\\":{\\\"syncID\\\":\\\"JKAkk-wUEUpX\\\",\\\"version\\\":1},\\\"clients\\\":{\\\"syncID\\\":\\\"KfANCdkZNOFJ\\\",\\\"version\\\":1},\\\"tabs\\\":{\\\"syncID\\\":\\\"604bXkw7dnUq\\\",\\\"version\\\":1}}}\"}");
editor.putLong("crypto5KeysLastModified", 1340401962760L);
putJSON(editor, "crypto5KeysServerResponseBody", "{\"ttl\":31536000,\"id\":\"keys\",\"payload\":\"{\\\"ciphertext\\\":\\\"+ZH6AaMhnKOWS7OzpdMfT5X2C7AYgax5JRd2HY4BHAFNPDv8\\\\\\/TwQIJgFDuNjASo0WEujjdkFot39qeQ24RLAz4D11rG\\\\\\/FZwo8FEUB9aSfec1N6sao6KzWkSamdqiJSRjpsUKexp2it1HvwqRDEBH\\\\\\/lgue11axv51u1MAV3ZfX2fdzVIiGTqF1YJAvENZtol3pyEh2HI4FZlv+oLW250nV4w1vAfDNGLVbbjXbdR+kec=\\\",\\\"IV\\\":\\\"bHqF\\\\\\/4PshKt2GQ\\\\\\/njGj2Jw==\\\",\\\"hmac\\\":\\\"f97c20d5c0a141f62a1571a108de1bad4b854b29c8d4b2b0d36da73421e4becc\\\"}\"}");
editor.putString("syncID", "Z2RopSDg-0bE");
editor.putString("clusterURL", "https://scl2-sync3.services.mozilla.com/");
putJSON(editor, "enabledEngineNames", "{\"history\":0,\"bookmarks\":0,\"passwords\":0,\"prefs\":0,\"addons\":0,\"forms\":0,\"clients\":0,\"tabs\":0}");
editor.putLong("serverClientsTimestamp", 1340401963950L);
editor.putLong("serverClientRecordTimestamp", 1340401963950L);
editor.commit();
}
/**
* Write a complete set of unversioned global prefs suitable for testing forward migration.
* @throws Exception
*/
public void populateGlobalSharedPrefs(final SharedPreferences to) throws Exception {
final Editor editor = to.edit();
editor.putLong("earliestnextsync", 1340402318649L);
editor.putBoolean("clusterurlisstale", false);
editor.commit();
}
/**
* Write a complete set of unversioned Account data suitable for testing forward migration.
* @throws Exception
*/
public void populateAccountData(final AccountManager accountManager, final Account account) throws Exception {
accountManager.setUserData(account, "account.guid", TEST_GUID);
accountManager.setUserData(account, "account.clientName", TEST_CLIENT_NAME);
accountManager.setUserData(account, "account.numClients", Long.valueOf(TEST_NUM_CLIENTS).toString());
}
public void testMigrateGlobals0and1() throws Exception {
final SharedPreferences v0a = new MockSharedPreferences();
final SharedPreferences v1a = new MockSharedPreferences();
final SharedPreferences v0b = new MockSharedPreferences();
final SharedPreferences v1b = new MockSharedPreferences();
populateGlobalSharedPrefs(v0a);
final int NUM_GLOBALS = 2;
assertEquals(NUM_GLOBALS, v0a.getAll().size());
assertEquals(NUM_GLOBALS, PublicMigrator.upgradeGlobals0to1(v0a, v1a));
assertEquals(NUM_GLOBALS, PublicMigrator.downgradeGlobals1to0(v1a, v0b));
assertEquals(NUM_GLOBALS, PublicMigrator.upgradeGlobals0to1(v0b, v1b));
assertEquals(v0a.getAll(), v0b.getAll());
assertEquals(v1a.getAll(), v1b.getAll());
}
public void testMigrateShared0and1() throws Exception {
final SharedPreferences v0a = new MockSharedPreferences();
final SharedPreferences v1a = new MockSharedPreferences();
final SharedPreferences v0b = new MockSharedPreferences();
final SharedPreferences v1b = new MockSharedPreferences();
populateAccountSharedPrefs(v0a);
final int NUM_GLOBALS = 24;
assertEquals(NUM_GLOBALS, v0a.getAll().size());
assertEquals(NUM_GLOBALS, PublicMigrator.upgradeShared0to1(v0a, v1a));
assertEquals(NUM_GLOBALS, PublicMigrator.downgradeShared1to0(v1a, v0b));
assertEquals(NUM_GLOBALS, PublicMigrator.upgradeShared0to1(v0b, v1b));
assertEquals(v0a.getAll(), v0b.getAll());
assertEquals(v1a.getAll(), v1b.getAll());
}
public void testMigrateAccount0and1() throws Exception {
final Context context = getApplicationContext();
final AccountManager accountManager = AccountManager.get(context);
final SyncAccountParameters syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, null);
Account account = null;
try {
account = SyncAccounts.createSyncAccount(syncAccount, false);
populateAccountData(accountManager, account);
final int NUM_ACCOUNTS = 3;
final SharedPreferences a = new MockSharedPreferences();
final SharedPreferences b = new MockSharedPreferences();
assertEquals(NUM_ACCOUNTS, PublicMigrator.upgradeAndroidAccount0to1(accountManager, account, a));
assertEquals(NUM_ACCOUNTS, a.getAll().size());
assertEquals(NUM_ACCOUNTS, PublicMigrator.downgradeAndroidAccount1to0(a, accountManager, account));
TestSyncAccounts.deleteAccount(this, accountManager, account);
account = SyncAccounts.createSyncAccount(syncAccount, false);
assertEquals(NUM_ACCOUNTS, PublicMigrator.downgradeAndroidAccount1to0(a, accountManager, account));
assertEquals(NUM_ACCOUNTS, PublicMigrator.upgradeAndroidAccount0to1(accountManager, account, b));
assertEquals(a.getAll(), b.getAll());
} finally {
if (account != null) {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
}
public void testMigrate0to1() throws Exception {
final Context context = getApplicationContext();
final String ACCOUNT_SHARED_PREFS_NAME = "sync.prefs.3qyu5zoqpuu4zhdiv5l2qthsiter3vop";
final String GLOBAL_SHARED_PREFS_NAME = "sync.prefs.global";
final String path = Utils.getPrefsPath(TEST_PRODUCT, TEST_USERNAME, TEST_SERVERURL, TEST_PROFILE, 0);
assertEquals(ACCOUNT_SHARED_PREFS_NAME, path);
final SharedPreferences accountPrefs = context.getSharedPreferences(ACCOUNT_SHARED_PREFS_NAME, Utils.SHARED_PREFERENCES_MODE);
final SharedPreferences globalPrefs = context.getSharedPreferences(GLOBAL_SHARED_PREFS_NAME, Utils.SHARED_PREFERENCES_MODE);
accountPrefs.edit().clear().commit();
globalPrefs.edit().clear().commit();
// Clear prefs we're about to write into.
final SharedPreferences existingPrefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME, TEST_SERVERURL, TEST_PROFILE, 1);
existingPrefs.edit().clear().commit();
final AccountManager accountManager = AccountManager.get(context);
final SyncAccountParameters syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, null);
Account account = null;
try {
account = SyncAccounts.createSyncAccount(syncAccount, false); // Wipes prefs.
populateAccountSharedPrefs(accountPrefs);
populateGlobalSharedPrefs(globalPrefs);
populateAccountData(accountManager, account);
ConfigurationMigrator.upgrade0to1(context, accountManager, account, TEST_PRODUCT, TEST_USERNAME, TEST_SERVERURL, TEST_PROFILE);
} finally {
if (account != null) {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
Map<String, ?> origAccountPrefs = accountPrefs.getAll();
Map<String, ?> origGlobalPrefs = globalPrefs.getAll();
assertFalse(origAccountPrefs.isEmpty());
assertFalse(origGlobalPrefs.isEmpty());
final SharedPreferences newPrefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME, TEST_SERVERURL, TEST_PROFILE, 1);
// Some global stuff.
assertEquals(false, newPrefs.getBoolean(SyncConfiguration.PREF_CLUSTER_URL_IS_STALE, true));
assertEquals(1340402318649L, newPrefs.getLong(SyncConfiguration.PREF_EARLIEST_NEXT_SYNC, 111));
// Some per-Sync account stuff.
assertEquals("{\"timestamp\":1340402003370}", newPrefs.getString("bookmarks.remote", null));
assertEquals("{\"timestamp\":1340402008397}", newPrefs.getString("bookmarks.local", null));
assertEquals("P8gG8ERuJ4H1", newPrefs.getString("bookmarks.syncID", null));
assertEquals(1340401961960L, newPrefs.getLong("metaGlobalLastModified", 0));
// Some per-Android account stuff.
assertEquals(TEST_GUID, newPrefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null));
assertEquals(TEST_CLIENT_NAME, newPrefs.getString(SyncConfiguration.PREF_CLIENT_NAME, null));
assertEquals(TEST_NUM_CLIENTS, newPrefs.getLong(SyncConfiguration.PREF_NUM_CLIENTS, -1L));
// Now try to downgrade.
accountPrefs.edit().clear().commit();
globalPrefs.edit().clear().commit();
account = null;
try {
account = SyncAccounts.createSyncAccount(syncAccount, false);
ConfigurationMigrator.downgrade1to0(context, accountManager, account, TEST_PRODUCT, TEST_USERNAME, TEST_SERVERURL, TEST_PROFILE);
final String V0_PREF_ACCOUNT_GUID = "account.guid";
final String V0_PREF_CLIENT_NAME = "account.clientName";
final String V0_PREF_NUM_CLIENTS = "account.numClients";
assertEquals(TEST_GUID, accountManager.getUserData(account, V0_PREF_ACCOUNT_GUID));
assertEquals(TEST_CLIENT_NAME, accountManager.getUserData(account, V0_PREF_CLIENT_NAME));
assertEquals(Long.valueOf(TEST_NUM_CLIENTS).toString(), accountManager.getUserData(account, V0_PREF_NUM_CLIENTS));
} finally {
if (account != null) {
TestSyncAccounts.deleteAccount(this, accountManager, account);
}
}
// Check re-constituted prefs against old prefs.
assertEquals(origAccountPrefs, accountPrefs.getAll());
assertEquals(origGlobalPrefs, globalPrefs.getAll());
}
}

Просмотреть файл

@ -0,0 +1,200 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.io.IOException;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.testhelpers.BaseMockServerSyncStage;
import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
import org.mozilla.gecko.background.testhelpers.MockRecord;
import org.mozilla.gecko.background.testhelpers.WBORepository;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import org.mozilla.gecko.sync.EngineSettings;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.MetaGlobalException;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.SyncConfigurationException;
import org.mozilla.gecko.sync.SynchronizerConfiguration;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.repositories.domain.Record;
import org.mozilla.gecko.sync.stage.NoSuchStageException;
import org.mozilla.gecko.sync.synchronizer.Synchronizer;
/**
* Test the on-device side effects of reset operations on a stage.
*
* See also "TestResetCommands" in the unit test suite.
*/
public class TestResetting extends AndroidSyncTestCase {
private static final String TEST_USERNAME = "johndoe";
private static final String TEST_PASSWORD = "password";
private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
@Override
public void setUp() {
assertTrue(WaitHelper.getTestWaiter().isIdle());
}
/**
* Set up a mock stage that synchronizes two mock repositories. Apply various
* reset/sync/wipe permutations and check state.
*/
public void testResetAndWipeStage() throws Exception {
final long startTime = System.currentTimeMillis();
final GlobalSessionCallback callback = createGlobalSessionCallback();
final GlobalSession session = createDefaultGlobalSession(callback);
final ExecutableMockServerSyncStage stage = new ExecutableMockServerSyncStage() {
@Override
public void onSynchronized(Synchronizer synchronizer) {
try {
assertTrue(startTime <= synchronizer.bundleA.getTimestamp());
assertTrue(startTime <= synchronizer.bundleB.getTimestamp());
// Call up to allow the usual persistence etc. to happen.
super.onSynchronized(synchronizer);
} catch (Throwable e) {
performNotify(e);
return;
}
performNotify();
}
};
final boolean bumpTimestamps = true;
WBORepository local = new WBORepository(bumpTimestamps);
WBORepository remote = new WBORepository(bumpTimestamps);
stage.name = "mock";
stage.collection = "mock";
stage.local = local;
stage.remote = remote;
stage.executeSynchronously(session);
// Verify the persisted values.
assertConfigTimestampsGreaterThan(stage.leakConfig(), startTime, startTime);
// Reset.
stage.resetLocal(session);
// Verify that they're gone.
assertConfigTimestampsEqual(stage.leakConfig(), 0, 0);
// Now sync data, ensure that timestamps come back.
final long afterReset = System.currentTimeMillis();
final String recordGUID = "abcdefghijkl";
local.wbos.put(recordGUID, new MockRecord(recordGUID, "mock", startTime, false));
// Sync again with data and verify timestamps and data.
stage.executeSynchronously(session);
assertConfigTimestampsGreaterThan(stage.leakConfig(), afterReset, afterReset);
assertEquals(1, remote.wbos.size());
assertEquals(1, local.wbos.size());
Record remoteRecord = remote.wbos.get(recordGUID);
assertNotNull(remoteRecord);
assertNotNull(local.wbos.get(recordGUID));
assertEquals(recordGUID, remoteRecord.guid);
assertTrue(afterReset <= remoteRecord.lastModified);
// Reset doesn't clear data.
stage.resetLocal(session);
assertConfigTimestampsEqual(stage.leakConfig(), 0, 0);
assertEquals(1, remote.wbos.size());
assertEquals(1, local.wbos.size());
remoteRecord = remote.wbos.get(recordGUID);
assertNotNull(remoteRecord);
assertNotNull(local.wbos.get(recordGUID));
// Wipe does. Recover from reset...
final long beforeWipe = System.currentTimeMillis();
stage.executeSynchronously(session);
assertEquals(1, remote.wbos.size());
assertEquals(1, local.wbos.size());
assertConfigTimestampsGreaterThan(stage.leakConfig(), beforeWipe, beforeWipe);
// ... then wipe.
stage.wipeLocal(session);
assertConfigTimestampsEqual(stage.leakConfig(), 0, 0);
assertEquals(1, remote.wbos.size()); // We don't wipe the server.
assertEquals(0, local.wbos.size()); // We do wipe local.
}
/**
* A stage that joins two Repositories with no wrapping.
*/
public class ExecutableMockServerSyncStage extends BaseMockServerSyncStage {
/**
* Run this stage synchronously.
*/
public void executeSynchronously(final GlobalSession session) {
final BaseMockServerSyncStage self = this;
performWait(new Runnable() {
@Override
public void run() {
try {
self.execute(session);
} catch (NoSuchStageException e) {
performNotify(e);
}
}
});
}
}
private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
return new GlobalSession(
SyncConfiguration.DEFAULT_USER_API,
null,
TEST_USERNAME, TEST_PASSWORD, null,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
callback, getApplicationContext(), null, null) {
@Override
public boolean engineIsEnabled(String engineName,
EngineSettings engineSettings)
throws MetaGlobalException {
return true;
}
@Override
public void advance() {
// So we don't proceed and run other stages.
}
};
}
private static GlobalSessionCallback createGlobalSessionCallback() {
return new DefaultGlobalSessionCallback() {
@Override
public void handleAborted(GlobalSession globalSession, String reason) {
performNotify(new Exception("Aborted"));
}
@Override
public void handleError(GlobalSession globalSession, Exception ex) {
performNotify(ex);
}
};
}
private static void assertConfigTimestampsGreaterThan(SynchronizerConfiguration config, long local, long remote) {
assertTrue(local <= config.localBundle.getTimestamp());
assertTrue(remote <= config.remoteBundle.getTimestamp());
}
private static void assertConfigTimestampsEqual(SynchronizerConfiguration config, long local, long remote) {
assertEquals(local, config.localBundle.getTimestamp());
assertEquals(remote, config.remoteBundle.getTimestamp());
}
}

Просмотреть файл

@ -0,0 +1,114 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.setup.activities.SendTabData;
import android.content.Intent;
/**
* These tests are on device because the Intent, Pattern, and Matcher APIs are
* stubs on desktop.
*/
public class TestSendTabData extends AndroidSyncTestCase {
protected static Intent makeShareIntent(String text, String subject, String title) {
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_TEXT, text);
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TITLE, title);
return intent;
}
// From Fennec:
//
// I/FxSync ( 7420): fennec :: SendTabActivity :: Send was clicked.
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.TEXT -> http://www.reddit.com/
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.SUBJECT -> reddit: the front page of the internet
public void testFennecBrowser() {
Intent shareIntent = makeShareIntent("http://www.reddit.com/",
"reddit: the front page of the internet",
null);
SendTabData fromIntent = SendTabData.fromIntent(shareIntent);
assertEquals("reddit: the front page of the internet", fromIntent.title);
assertEquals("http://www.reddit.com/", fromIntent.uri);
}
// From Android Browser:
//
// I/FxSync ( 7420): fennec :: SendTabActivity :: Send was clicked.
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.TEXT -> http://bl176w.blu176.mail.live.com/m/messages.m/?mid=m95277577-e5a5-11e1-bfeb-00237de49bb0&mts=2012-08-14T00%3a18%3a44.390Z&fid=00000000-0000-0000-0000-000000000001&iru=%2fm%2ffolders.m%2f&pmid=m173216c1-e5ea-11e1-bac7-002264c17c66&pmts=2012-08-14T08%3a29%3a01.057Z&nmid=m0e0a4a3a-e511-11e1-bfe5-00237de3362a&nmts=2012-08-13T06%3a44%3a51.910Z
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.SUBJECT -> Hotmail: ONLY SIX PERFORMANCES LEFT! SPECIAL SECOND SHOW OFFER - GET $
public void testAndroidBrowser() {
Intent shareIntent = makeShareIntent("http://www.reddit.com/",
"reddit: the front page of the internet",
null);
SendTabData fromIntent = SendTabData.fromIntent(shareIntent);
assertEquals("reddit: the front page of the internet", fromIntent.title);
assertEquals("http://www.reddit.com/", fromIntent.uri);
}
// From Pocket:
//
// I/FxSync ( 7420): fennec :: SendTabActivity :: Send was clicked.
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.TEXT -> http://t.co/bfsbM2oV
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.SUBJECT -> Launching the Canadian OGP Civil Society Discussion Group
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.TITLE -> Launching the Canadian OGP Civil Society Discussion Group
public void testPocket() {
Intent shareIntent = makeShareIntent("http://t.co/bfsbM2oV",
"Launching the Canadian OGP Civil Society Discussion Group",
"Launching the Canadian OGP Civil Society Discussion Group");
SendTabData fromIntent = SendTabData.fromIntent(shareIntent);
assertEquals("Launching the Canadian OGP Civil Society Discussion Group", fromIntent.title);
assertEquals("http://t.co/bfsbM2oV", fromIntent.uri);
}
// A couple of examples from Twitter App:
//
// I/FxSync ( 7420): fennec :: SendTabActivity :: Send was clicked.
// I/FxSync (17610): fennec :: SendTabActivity :: android.intent.extra.TEXT = Cory Doctorow (@doctorow) tweeted at 11:21 AM on Sat, Jan 12, 2013:
// I/FxSync (17610): Pls RT: @lessig on the DoJ's vindictive prosecution of Aaron Swartz http://t.co/qNalE70n #aaronsw
// I/FxSync (17610): (https://twitter.com/doctorow/status/290176681065451520)
// I/FxSync (17610):
// I/FxSync (17610): Get the official Twitter app at https://twitter.com/download
// I/FxSync (17610): fennec :: SendTabActivity :: android.intent.extra.SUBJECT = Tweet from Cory Doctorow (@doctorow)
//
// I/FxSync ( 7420): fennec :: SendTabActivity :: Send was clicked.
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.TEXT -> David Eaves (@daeaves) tweeted at 0:08 PM on Fri, Jan 11, 2013:
// I/FxSync ( 7420): New on eaves.ca: Launching the Canadian OGP Civil Society Discussion Group http://t.co/bfsbM2oV
// I/FxSync ( 7420): (https://twitter.com/daeaves/status/289826143723466752)
// I/FxSync ( 7420):
// I/FxSync ( 7420): Get the official Twitter app at https://twitter.com/download
// I/FxSync ( 7420): fennec :: SendTabActivity :: android.intent.extra.SUBJECT -> Tweet from David Eaves (@daeaves)
public void testTwitter() {
Intent shareIntent1 = makeShareIntent("Cory Doctorow (@doctorow) tweeted at 11:21 AM on Sat, Jan 12, 2013:\n" +
"Pls RT: @lessig on the DoJ's vindictive prosecution of Aaron Swartz http://t.co/qNalE70n #aaronsw\n" +
"(https://twitter.com/doctorow/status/290176681065451520)\n" +
"\n" +
"Get the official Twitter app at https://twitter.com/download",
"Tweet from Cory Doctorow (@doctorow)",
null);
SendTabData fromIntent1 = SendTabData.fromIntent(shareIntent1);
assertEquals("Tweet from Cory Doctorow (@doctorow)", fromIntent1.title);
assertEquals("http://t.co/qNalE70n", fromIntent1.uri);
Intent shareIntent2 = makeShareIntent("David Eaves (@daeaves) tweeted at 0:08 PM on Fri, Jan 11, 2013:\n" +
"New on eaves.ca: Launching the Canadian OGP Civil Society Discussion Group http://t.co/bfsbM2oV\n" +
"(https://twitter.com/daeaves/status/289826143723466752)\n" +
"\n" +
"Get the official Twitter app at https://twitter.com/download",
"Tweet from David Eaves (@daeaves)",
null);
SendTabData fromIntent2 = SendTabData.fromIntent(shareIntent2);
assertEquals("Tweet from David Eaves (@daeaves)", fromIntent2.title);
assertEquals("http://t.co/bfsbM2oV", fromIntent2.uri);
}
}

Просмотреть файл

@ -0,0 +1,377 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import junit.framework.AssertionFailedError;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate;
import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate;
import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate;
import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate;
import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate;
import org.mozilla.gecko.background.testhelpers.WBORepository;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import org.mozilla.gecko.sync.synchronizer.Synchronizer;
import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
import android.content.Context;
public class TestStoreTracking extends AndroidSyncTestCase {
public void assertEq(Object expected, Object actual) {
try {
assertEquals(expected, actual);
} catch (AssertionFailedError e) {
performNotify(e);
}
}
public class TrackingWBORepository extends WBORepository {
@Override
public synchronized boolean shouldTrack() {
return true;
}
}
public void doTestStoreRetrieveByGUID(final WBORepository repository,
final RepositorySession session,
final String expectedGUID,
final Record record) {
final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() {
@Override
public void onRecordStoreSucceeded(String guid) {
Logger.debug(getName(), "Stored " + guid);
assertEq(expectedGUID, guid);
}
@Override
public void onStoreCompleted(long storeEnd) {
Logger.debug(getName(), "Store completed at " + storeEnd + ".");
try {
session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() {
@Override
public void onFetchedRecord(Record record) {
Logger.debug(getName(), "Hurrah! Fetched record " + record.guid);
assertEq(expectedGUID, record.guid);
}
@Override
public void onFetchCompleted(final long fetchEnd) {
Logger.debug(getName(), "Fetch completed at " + fetchEnd + ".");
// But fetching by time returns nothing.
session.fetchSince(0, new SimpleSuccessFetchDelegate() {
private AtomicBoolean fetched = new AtomicBoolean(false);
@Override
public void onFetchedRecord(Record record) {
Logger.debug(getName(), "Fetched record " + record.guid);
fetched.set(true);
performNotify(new AssertionFailedError("Should have fetched no record!"));
}
@Override
public void onFetchCompleted(final long fetchEnd) {
if (fetched.get()) {
Logger.debug(getName(), "Not finishing session: record retrieved.");
return;
}
try {
session.finish(new SimpleSuccessFinishDelegate() {
@Override
public void onFinishSucceeded(RepositorySession session,
RepositorySessionBundle bundle) {
performNotify();
}
});
} catch (InactiveSessionException e) {
performNotify(e);
}
}
});
}
});
} catch (InactiveSessionException e) {
performNotify(e);
}
}
};
session.setStoreDelegate(storeDelegate);
try {
Logger.debug(getName(), "Storing...");
session.store(record);
session.storeDone();
} catch (NoStoreDelegateException e) {
// Should not happen.
}
}
private void doTestNewSessionRetrieveByTime(final WBORepository repository,
final String expectedGUID) {
final SimpleSuccessCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() {
@Override
public void onSessionCreated(final RepositorySession session) {
Logger.debug(getName(), "Session created.");
try {
session.begin(new SimpleSuccessBeginDelegate() {
@Override
public void onBeginSucceeded(final RepositorySession session) {
// Now we get a result.
session.fetchSince(0, new SimpleSuccessFetchDelegate() {
@Override
public void onFetchedRecord(Record record) {
assertEq(expectedGUID, record.guid);
}
@Override
public void onFetchCompleted(long end) {
try {
session.finish(new SimpleSuccessFinishDelegate() {
@Override
public void onFinishSucceeded(RepositorySession session,
RepositorySessionBundle bundle) {
// Hooray!
performNotify();
}
});
} catch (InactiveSessionException e) {
performNotify(e);
}
}
});
}
});
} catch (InvalidSessionTransitionException e) {
performNotify(e);
}
}
};
Runnable create = new Runnable() {
@Override
public void run() {
repository.createSession(createDelegate, getApplicationContext());
}
};
performWait(create);
}
/**
* Store a record in one session. Verify that fetching by GUID returns
* the record. Verify that fetching by timestamp fails to return records.
* Start a new session. Verify that fetching by timestamp returns the
* stored record.
*
* Invokes doTestStoreRetrieveByGUID, doTestNewSessionRetrieveByTime.
*/
public void testStoreRetrieveByGUID() {
Logger.debug(getName(), "Started.");
final WBORepository r = new TrackingWBORepository();
final long now = System.currentTimeMillis();
final String expectedGUID = "abcdefghijkl";
final Record record = new BookmarkRecord(expectedGUID, "bookmarks", now , false);
final RepositorySessionCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() {
@Override
public void onSessionCreated(RepositorySession session) {
Logger.debug(getName(), "Session created: " + session);
try {
session.begin(new SimpleSuccessBeginDelegate() {
@Override
public void onBeginSucceeded(final RepositorySession session) {
doTestStoreRetrieveByGUID(r, session, expectedGUID, record);
}
});
} catch (InvalidSessionTransitionException e) {
performNotify(e);
}
}
};
final Context applicationContext = getApplicationContext();
// This has to happen on a new thread so that we
// can wait for it!
Runnable create = onThreadRunnable(new Runnable() {
@Override
public void run() {
r.createSession(createDelegate, applicationContext);
}
});
Runnable retrieve = onThreadRunnable(new Runnable() {
@Override
public void run() {
doTestNewSessionRetrieveByTime(r, expectedGUID);
performNotify();
}
});
performWait(create);
performWait(retrieve);
}
private Runnable onThreadRunnable(final Runnable r) {
return new Runnable() {
@Override
public void run() {
new Thread(r).start();
}
};
}
public class CountingWBORepository extends TrackingWBORepository {
public AtomicLong counter = new AtomicLong(0L);
public class CountingWBORepositorySession extends WBORepositorySession {
private static final String LOG_TAG = "CountingRepoSession";
public CountingWBORepositorySession(WBORepository repository) {
super(repository);
}
@Override
public void store(final Record record) throws NoStoreDelegateException {
Logger.debug(LOG_TAG, "Counter now " + counter.incrementAndGet());
super.store(record);
}
}
@Override
public void createSession(RepositorySessionCreationDelegate delegate,
Context context) {
delegate.deferredCreationDelegate().onSessionCreated(new CountingWBORepositorySession(this));
}
}
public class TestRecord extends Record {
public TestRecord(String guid, String collection, long lastModified,
boolean deleted) {
super(guid, collection, lastModified, deleted);
}
@Override
public void initFromEnvelope(CryptoRecord payload) {
return;
}
@Override
public CryptoRecord getEnvelope() {
return null;
}
@Override
protected void populatePayload(ExtendedJSONObject payload) {
}
@Override
protected void initFromPayload(ExtendedJSONObject payload) {
}
@Override
public Record copyWithIDs(String guid, long androidID) {
return new TestRecord(guid, this.collection, this.lastModified, this.deleted);
}
}
/**
* Create two repositories, syncing from one to the other. Ensure
* that records stored from one aren't re-uploaded.
*/
public void testStoreBetweenRepositories() {
final CountingWBORepository repoA = new CountingWBORepository(); // "Remote". First source.
final CountingWBORepository repoB = new CountingWBORepository(); // "Local". First sink.
long now = System.currentTimeMillis();
TestRecord recordA1 = new TestRecord("aacdefghiaaa", "coll", now - 30, false);
TestRecord recordA2 = new TestRecord("aacdefghibbb", "coll", now - 20, false);
TestRecord recordB1 = new TestRecord("aacdefghiaaa", "coll", now - 10, false);
TestRecord recordB2 = new TestRecord("aacdefghibbb", "coll", now - 40, false);
TestRecord recordA3 = new TestRecord("nncdefghibbb", "coll", now, false);
TestRecord recordB3 = new TestRecord("nncdefghiaaa", "coll", now, false);
// A1 and B1 are the same, but B's version is newer. We expect A1 to be downloaded
// and B1 to be uploaded.
// A2 and B2 are the same, but A's version is newer. We expect A2 to be downloaded
// and B2 to not be uploaded.
// Both A3 and B3 are new. We expect them to go in each direction.
// Expected counts, then:
// Repo A: B1 + B3
// Repo B: A1 + A2 + A3
repoB.wbos.put(recordB1.guid, recordB1);
repoB.wbos.put(recordB2.guid, recordB2);
repoB.wbos.put(recordB3.guid, recordB3);
repoA.wbos.put(recordA1.guid, recordA1);
repoA.wbos.put(recordA2.guid, recordA2);
repoA.wbos.put(recordA3.guid, recordA3);
final Synchronizer s = new Synchronizer();
s.repositoryA = repoA;
s.repositoryB = repoB;
Runnable r = new Runnable() {
@Override
public void run() {
s.synchronize(getApplicationContext(), new SynchronizerDelegate() {
@Override
public void onSynchronized(Synchronizer synchronizer) {
long countA = repoA.counter.get();
long countB = repoB.counter.get();
Logger.debug(getName(), "Counts: " + countA + ", " + countB);
assertEq(2L, countA);
assertEq(3L, countB);
// Testing for store timestamp 'hack'.
// We fetched from A first, and so its bundle timestamp will be the last
// stored time. We fetched from B second, so its bundle timestamp will be
// the last fetched time.
final long timestampA = synchronizer.bundleA.getTimestamp();
final long timestampB = synchronizer.bundleB.getTimestamp();
Logger.debug(getName(), "Repo A timestamp: " + timestampA);
Logger.debug(getName(), "Repo B timestamp: " + timestampB);
Logger.debug(getName(), "Repo A fetch done: " + repoA.stats.fetchCompleted);
Logger.debug(getName(), "Repo A store done: " + repoA.stats.storeCompleted);
Logger.debug(getName(), "Repo B fetch done: " + repoB.stats.fetchCompleted);
Logger.debug(getName(), "Repo B store done: " + repoB.stats.storeCompleted);
assertTrue(timestampB <= timestampA);
assertTrue(repoA.stats.fetchCompleted <= timestampA);
assertTrue(repoA.stats.storeCompleted >= repoA.stats.fetchCompleted);
assertEquals(repoA.stats.storeCompleted, timestampA);
assertEquals(repoB.stats.fetchCompleted, timestampB);
performNotify();
}
@Override
public void onSynchronizeFailed(Synchronizer synchronizer,
Exception lastException, String reason) {
Logger.debug(getName(), "Failed.");
performNotify(new AssertionFailedError("Should not fail."));
}
});
}
};
performWait(onThreadRunnable(r));
}
}

Просмотреть файл

@ -0,0 +1,343 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.test.InstrumentationTestCase;
/**
* We can use <code>performWait</code> and <code>performNotify</code> here if we
* are careful about threading issues with <code>AsyncTask</code>. We need to
* take some care to both create and run certain tasks on the main thread --
* moving the object allocation out of the UI thread causes failures!
* <p>
* @see "<a href='http://stackoverflow.com/questions/2321829/android-asynctask-testing-problem-with-android-test-framework'>
* http://stackoverflow.com/questions/2321829/android-asynctask-testing-problem-with-android-test-framework</a>."
*/
public class TestSyncAccounts extends AndroidSyncTestCase {
private static final String TEST_USERNAME = "testAccount@mozilla.com";
private static final String TEST_SYNCKEY = "testSyncKey";
private static final String TEST_PASSWORD = "testPassword";
private static final String TEST_SERVERURL = "test.server.url/";
private static final String TEST_CLUSTERURL = "test.cluster.url/";
public static final String TEST_ACCOUNTTYPE = SyncConstants.ACCOUNTTYPE_SYNC;
public static final String TEST_PRODUCT = GlobalConstants.BROWSER_INTENT_PACKAGE;
public static final String TEST_PROFILE = Constants.DEFAULT_PROFILE;
public static final long TEST_VERSION = SyncConfiguration.CURRENT_PREFS_VERSION;
public static final String TEST_PREFERENCE = "testPreference";
public static final String TEST_SYNC_ID = "testSyncID";
private Account account;
private Context context;
private AccountManager accountManager;
private SyncAccountParameters syncAccount;
public void setUp() {
account = null;
context = getApplicationContext();
accountManager = AccountManager.get(context);
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, null);
}
public static void deleteAccount(final InstrumentationTestCase test, final AccountManager accountManager, final Account account) {
performWait(new Runnable() {
@Override
public void run() {
try {
test.runTestOnUiThread(new Runnable() {
final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> future) {
try {
future.getResult(5L, TimeUnit.SECONDS);
} catch (Exception e) {
}
performNotify();
}
};
@Override
public void run() {
accountManager.removeAccount(account, callback, null);
}
});
} catch (Throwable e) {
performNotify(e);
}
}
});
}
public void tearDown() {
if (account == null) {
return;
}
deleteAccount(this, accountManager, account);
account = null;
}
public void testSyncAccountParameters() {
assertEquals(TEST_USERNAME, syncAccount.username);
assertNull(syncAccount.accountManager);
assertNull(syncAccount.serverURL);
try {
syncAccount = new SyncAccountParameters(context, null,
null, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL);
} catch (IllegalArgumentException e) {
return;
} catch (Exception e) {
fail("Did not expect exception: " + e);
}
fail("Expected IllegalArgumentException.");
}
public void testCreateAccount() {
int before = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
account = SyncAccounts.createSyncAccount(syncAccount, false);
int afterCreate = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
assertTrue(afterCreate > before);
deleteAccount(this, accountManager, account);
account = null;
int afterDelete = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
assertEquals(before, afterDelete);
}
public void testCreateSecondAccount() {
int before = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
account = SyncAccounts.createSyncAccount(syncAccount, false);
int afterFirst = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
assertTrue(afterFirst > before);
SyncAccountParameters secondSyncAccount = new SyncAccountParameters(context, null,
"second@username.com", TEST_SYNCKEY, TEST_PASSWORD, null);
Account second = SyncAccounts.createSyncAccount(secondSyncAccount, false);
assertNotNull(second);
int afterSecond = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
assertTrue(afterSecond > afterFirst);
deleteAccount(this, accountManager, second);
deleteAccount(this, accountManager, account);
account = null;
int afterDelete = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
assertEquals(before, afterDelete);
}
public void testCreateDuplicateAccount() {
int before = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
account = SyncAccounts.createSyncAccount(syncAccount, false);
int afterCreate = accountManager.getAccountsByType(TEST_ACCOUNTTYPE).length;
assertTrue(afterCreate > before);
Account dupe = SyncAccounts.createSyncAccount(syncAccount, false);
assertNull(dupe);
}
public void testClientRecord() throws NoSuchAlgorithmException, UnsupportedEncodingException {
final String TEST_NAME = "testName";
final String TEST_GUID = "testGuid";
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, null, null, TEST_NAME, TEST_GUID);
account = SyncAccounts.createSyncAccount(syncAccount, false);
assertNotNull(account);
SharedPreferences prefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME,
SyncAccounts.DEFAULT_SERVER, TEST_PROFILE, TEST_VERSION);
// Verify that client record is set.
assertEquals(TEST_GUID, prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null));
assertEquals(TEST_NAME, prefs.getString(SyncConfiguration.PREF_CLIENT_NAME, null));
// Let's verify that clusterURL is correctly not set.
String clusterURL = prefs.getString(SyncConfiguration.PREF_CLUSTER_URL, null);
assertNull(clusterURL);
}
public void testClusterURL() throws NoSuchAlgorithmException, UnsupportedEncodingException {
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL, TEST_CLUSTERURL, null, null);
account = SyncAccounts.createSyncAccount(syncAccount, false);
assertNotNull(account);
SharedPreferences prefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME,
TEST_SERVERURL, TEST_PROFILE, TEST_VERSION);
String clusterURL = prefs.getString(SyncConfiguration.PREF_CLUSTER_URL, null);
assertNotNull(clusterURL);
assertEquals(TEST_CLUSTERURL, clusterURL);
// Let's verify that client name and GUID are not set.
assertNull(prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null));
assertNull(prefs.getString(SyncConfiguration.PREF_CLIENT_NAME, null));
}
/**
* Verify that creating an account wipes stale settings in Shared Preferences.
*/
public void testCreatingWipesSharedPrefs() throws Exception {
final String TEST_PREFERENCE = "testPreference";
final String TEST_SYNC_ID = "testSyncID";
SharedPreferences prefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME,
TEST_SERVERURL, TEST_PROFILE, TEST_VERSION);
prefs.edit().putString(SyncConfiguration.PREF_SYNC_ID, TEST_SYNC_ID).commit();
prefs.edit().putString(TEST_PREFERENCE, TEST_SYNC_ID).commit();
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL);
account = SyncAccounts.createSyncAccount(syncAccount, false);
assertNotNull(account);
// All values deleted (known and unknown).
assertNull(prefs.getString(SyncConfiguration.PREF_SYNC_ID, null));
assertNull(prefs.getString(TEST_SYNC_ID, null));
}
/**
* Verify that creating an account preserves settings in Shared Preferences when asked.
*/
public void testCreateSyncAccountWithExistingPreferences() throws Exception {
SharedPreferences prefs = Utils.getSharedPreferences(context, TEST_PRODUCT, TEST_USERNAME,
TEST_SERVERURL, TEST_PROFILE, TEST_VERSION);
prefs.edit().putString(SyncConfiguration.PREF_SYNC_ID, TEST_SYNC_ID).commit();
prefs.edit().putString(TEST_PREFERENCE, TEST_SYNC_ID).commit();
assertNotNull(prefs.getString(TEST_PREFERENCE, null));
assertNotNull(prefs.getString(SyncConfiguration.PREF_SYNC_ID, null));
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL);
account = SyncAccounts.createSyncAccountPreservingExistingPreferences(syncAccount, false);
assertNotNull(account);
// All values remain (known and unknown).
assertNotNull(prefs.getString(TEST_PREFERENCE, null));
assertNotNull(prefs.getString(SyncConfiguration.PREF_SYNC_ID, null));
}
protected void assertParams(final SyncAccountParameters params) throws Exception {
assertNotNull(params);
assertEquals(context, params.context);
assertEquals(Utils.usernameFromAccount(TEST_USERNAME), params.username);
assertEquals(TEST_PASSWORD, params.password);
assertEquals(TEST_SERVERURL, params.serverURL);
assertEquals(TEST_SYNCKEY, params.syncKey);
}
public void testBlockingFromAndroidAccountV0() throws Throwable {
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL, TEST_CLUSTERURL, null, null);
try {
account = SyncAccounts.createSyncAccount(syncAccount);
assertNotNull(account);
// Test fetching parameters multiple times. Historically, we needed to
// invalidate this token type every fetch; now we don't, but we'd like
// to ensure multiple fetches work.
SyncAccountParameters params = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
assertParams(params);
params = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
assertParams(params);
// Test this works on the main thread.
this.runTestOnUiThread(new Runnable() {
@Override
public void run() {
SyncAccountParameters params;
try {
params = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
assertParams(params);
} catch (Exception e) {
fail("Fetching Sync account parameters failed on UI thread.");
}
}
});
} finally {
if (account != null) {
deleteAccount(this, accountManager, account);
account = null;
}
}
}
public void testMakeSyncAccountDeletedIntent() throws Throwable {
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL, TEST_CLUSTERURL, null, null);
try {
account = SyncAccounts.createSyncAccount(syncAccount);
assertNotNull(account);
Intent intent = SyncAccounts.makeSyncAccountDeletedIntent(context, accountManager, account);
assertEquals(SyncConstants.SYNC_ACCOUNT_DELETED_ACTION, intent.getAction());
assertEquals(SyncConstants.SYNC_ACCOUNT_DELETED_INTENT_VERSION, intent.getLongExtra(Constants.JSON_KEY_VERSION, 0));
assertEquals(TEST_USERNAME, intent.getStringExtra(Constants.JSON_KEY_ACCOUNT));
assertTrue(Math.abs(intent.getLongExtra(Constants.JSON_KEY_TIMESTAMP, 0) - System.currentTimeMillis()) < 1000);
String payload = intent.getStringExtra(Constants.JSON_KEY_PAYLOAD);
assertNotNull(payload);
SyncAccountParameters params = new SyncAccountParameters(context, accountManager, ExtendedJSONObject.parseJSONObject(payload));
// Can't use assertParams because Sync key is deleted.
assertNotNull(params);
assertEquals(context, params.context);
assertEquals(Utils.usernameFromAccount(TEST_USERNAME), params.username);
assertEquals(TEST_PASSWORD, params.password);
assertEquals(TEST_SERVERURL, params.serverURL);
assertEquals("", params.syncKey);
} finally {
if (account != null) {
deleteAccount(this, accountManager, account);
account = null;
}
}
}
public void testBlockingPrefsFromAndroidAccountV0() throws Exception {
// Create test account with prefs. We use a different username to avoid a
// timing issue, where the delayed clean-up of the account created by the
// previous test deletes the preferences for this account.
SharedPreferences prefs = Utils.getSharedPreferences(context, TEST_PRODUCT,
TEST_USERNAME + "2", TEST_SERVERURL, TEST_PROFILE, TEST_VERSION);
prefs.edit().putString(TEST_PREFERENCE, TEST_SYNC_ID).commit();
syncAccount = new SyncAccountParameters(context, null,
TEST_USERNAME + "2", TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL);
account = SyncAccounts.createSyncAccountPreservingExistingPreferences(syncAccount, false);
assertNotNull(account);
// Fetch account and check prefs.
SharedPreferences sharedPreferences = SyncAccounts.blockingPrefsFromAndroidAccountV0(context, accountManager,
account, TEST_PRODUCT, TEST_PROFILE, TEST_VERSION);
assertNotNull(sharedPreferences);
assertEquals(TEST_SYNC_ID, sharedPreferences.getString(TEST_PREFERENCE, null));
}
}

Просмотреть файл

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import org.mozilla.gecko.sync.setup.SyncAuthenticatorService;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Bundle;
public class TestSyncAuthenticatorService extends AndroidSyncTestCase {
private static final String TEST_USERNAME = "testAccount@mozilla.com";
private static final String TEST_SYNCKEY = "testSyncKey";
private static final String TEST_PASSWORD = "testPassword";
private static final String TEST_SERVERURL = "test.server.url/";
private Account account;
private Context context;
private AccountManager accountManager;
private SyncAccountParameters syncAccount;
public void setUp() {
account = null;
context = getApplicationContext();
accountManager = AccountManager.get(context);
syncAccount = new SyncAccountParameters(context, accountManager,
TEST_USERNAME, TEST_SYNCKEY, TEST_PASSWORD, TEST_SERVERURL);
}
public void tearDown() {
if (account == null) {
return;
}
TestSyncAccounts.deleteAccount(this, accountManager, account);
account = null;
}
public void testGetPlainAuthToken() throws Exception {
account = SyncAccounts.createSyncAccount(syncAccount, false);
assertNotNull(account);
final Bundle bundle = SyncAuthenticatorService.getPlainAuthToken(context, account);
assertEquals(TEST_USERNAME, bundle.getString(AccountManager.KEY_ACCOUNT_NAME));
assertEquals(SyncConstants.ACCOUNTTYPE_SYNC, bundle.getString(AccountManager.KEY_ACCOUNT_TYPE));
assertEquals(Utils.usernameFromAccount(TEST_USERNAME), bundle.getString(Constants.OPTION_USERNAME));
assertEquals(TEST_PASSWORD, bundle.getString(AccountManager.KEY_AUTHTOKEN));
assertEquals(TEST_SYNCKEY, bundle.getString(Constants.OPTION_SYNCKEY));
assertEquals(TEST_SERVERURL, bundle.getString(Constants.OPTION_SERVER));
}
public void testGetBadTokenType() throws Exception {
account = SyncAccounts.createSyncAccount(syncAccount, false);
assertNotNull(account);
assertNull(accountManager.blockingGetAuthToken(account, "BAD_TOKEN_TYPE", false));
}
}

Просмотреть файл

@ -0,0 +1,119 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.PrefsSource;
import org.mozilla.gecko.sync.SyncConfiguration;
import android.content.SharedPreferences;
public class TestSyncConfiguration extends AndroidSyncTestCase implements PrefsSource {
public static final String TEST_PREFS_NAME = "test";
/*
* PrefsSource methods.
*/
@Override
public SharedPreferences getPrefs(String name, int mode) {
return this.getApplicationContext().getSharedPreferences(name, mode);
}
public void testEnabledEngineNames() {
SyncConfiguration config = null;
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
config = new SyncConfiguration(TEST_PREFS_NAME, this);
config.enabledEngineNames = new HashSet<String>();
config.enabledEngineNames.add("test1");
config.enabledEngineNames.add("test2");
config.persistToPrefs();
assertTrue(prefs.contains(SyncConfiguration.PREF_ENABLED_ENGINE_NAMES));
config = new SyncConfiguration(TEST_PREFS_NAME, this);
Set<String> expected = new HashSet<String>();
for (String name : new String[] { "test1", "test2" }) {
expected.add(name);
}
assertEquals(expected, config.enabledEngineNames);
config.enabledEngineNames = null;
config.persistToPrefs();
assertFalse(prefs.contains(SyncConfiguration.PREF_ENABLED_ENGINE_NAMES));
config = new SyncConfiguration(TEST_PREFS_NAME, this);
assertNull(config.enabledEngineNames);
}
public void testSyncID() {
SyncConfiguration config = null;
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
config = new SyncConfiguration(TEST_PREFS_NAME, this);
config.syncID = "test1";
config.persistToPrefs();
assertTrue(prefs.contains(SyncConfiguration.PREF_SYNC_ID));
config = new SyncConfiguration(TEST_PREFS_NAME, this);
assertEquals("test1", config.syncID);
}
public void testStoreSelectedEnginesToPrefs() {
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
// Store engines, excluding history/forms special case.
Map<String, Boolean> expectedEngines = new HashMap<String, Boolean>();
expectedEngines.put("test1", true);
expectedEngines.put("test2", false);
expectedEngines.put("test3", true);
SyncConfiguration.storeSelectedEnginesToPrefs(prefs, expectedEngines);
// Read values from selectedEngines.
assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC));
SyncConfiguration config = null;
config = new SyncConfiguration(TEST_PREFS_NAME, this);
config.loadFromPrefs(prefs);
assertEquals(expectedEngines, config.userSelectedEngines);
}
/**
* Tests dependency of forms engine on history engine.
*/
public void testSelectedEnginesHistoryAndForms() {
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
// Store engines, excluding history/forms special case.
Map<String, Boolean> storedEngines = new HashMap<String, Boolean>();
storedEngines.put("history", true);
SyncConfiguration.storeSelectedEnginesToPrefs(prefs, storedEngines);
// Expected engines.
storedEngines.put("forms", true);
// Read values from selectedEngines.
assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC));
SyncConfiguration config = null;
config = new SyncConfiguration(TEST_PREFS_NAME, this);
config.loadFromPrefs(prefs);
assertEquals(storedEngines, config.userSelectedEngines);
}
public void testsSelectedEnginesNoHistoryNorForms() {
SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0);
// Store engines, excluding history/forms special case.
Map<String, Boolean> storedEngines = new HashMap<String, Boolean>();
storedEngines.put("forms", true);
SyncConfiguration.storeSelectedEnginesToPrefs(prefs, storedEngines);
// Read values from selectedEngines.
assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC));
SyncConfiguration config = null;
config = new SyncConfiguration(TEST_PREFS_NAME, this);
config.loadFromPrefs(prefs);
// Forms should not be selected if history is not present.
assertTrue(config.userSelectedEngines.isEmpty());
}
}

Просмотреть файл

@ -0,0 +1,98 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import org.mozilla.gecko.background.db.CursorDumper;
import org.mozilla.gecko.background.db.TestFennecTabsStorage;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
import android.content.ContentProviderClient;
import android.database.Cursor;
public class TestTabsRecord extends TestFennecTabsStorage {
public void testTabsRecordFromCursor() throws Exception {
final ContentProviderClient tabsClient = getTabsClient();
deleteAllTestTabs(tabsClient);
insertSomeTestTabs(tabsClient);
final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
Cursor cursor = null;
try {
cursor = tabsClient.query(BrowserContract.Tabs.CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
assertEquals(3, cursor.getCount());
cursor.moveToPosition(1);
final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
// Make sure we clean up after ourselves.
assertEquals(1, cursor.getPosition());
assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
assertEquals(3, tabsRecord.tabs.size());
assertEquals(testTab1, tabsRecord.tabs.get(0));
assertEquals(testTab2, tabsRecord.tabs.get(1));
assertEquals(testTab3, tabsRecord.tabs.get(2));
assertEquals(Math.max(Math.max(testTab1.lastUsed, testTab2.lastUsed), testTab3.lastUsed), tabsRecord.lastModified);
} finally {
cursor.close();
}
}
// Verify that we can fetch a record when there are no local tabs at all.
public void testEmptyTabsRecordFromCursor() throws Exception {
final ContentProviderClient tabsClient = getTabsClient();
deleteAllTestTabs(tabsClient);
final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
Cursor cursor = null;
try {
cursor = tabsClient.query(BrowserContract.Tabs.CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending);
assertEquals(0, cursor.getCount());
final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
assertNotNull(tabsRecord.tabs);
assertEquals(0, tabsRecord.tabs.size());
assertEquals(0, tabsRecord.lastModified);
} finally {
cursor.close();
}
}
// Not much of a test, but verifies the tabs record at least agrees with the
// disk data and doubles as a database inspector.
public void testLocalTabs() throws Exception {
final ContentProviderClient tabsClient = getTabsClient();
final String positionAscending = BrowserContract.Tabs.POSITION + " ASC";
Cursor cursor = null;
try {
// Keep this in sync with the Fennec schema.
cursor = tabsClient.query(BrowserContract.Tabs.CONTENT_URI, null, BrowserContract.Tabs.CLIENT_GUID + " IS NULL", null, positionAscending);
CursorDumper.dumpCursor(cursor);
final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME);
assertEquals(TEST_CLIENT_GUID, tabsRecord.guid);
assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName);
assertNotNull(tabsRecord.tabs);
assertEquals(cursor.getCount(), tabsRecord.tabs.size());
} finally {
cursor.close();
}
}
}

Просмотреть файл

@ -0,0 +1,236 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.SyncConfigurationException;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.entity.StringEntity;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
/**
* When syncing and a server responds with a 400 "Upgrade Required," Sync
* accounts should be disabled.
*
* (We are not testing for package updating, because MY_PACKAGE_REPLACED
* broadcasts can only be sent by the system. Testing for package replacement
* needs to be done manually on a device.)
*
* @author liuche
*
*/
public class TestUpgradeRequired extends AndroidSyncTestCase {
private final String TEST_SERVER = "http://test.ser.ver/";
private static final String TEST_USERNAME = "user1";
private static final String TEST_PASSWORD = "pass1";
private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
private Context context;
public static boolean syncsAutomatically(Account a) {
return ContentResolver.getSyncAutomatically(a, BrowserContract.AUTHORITY);
}
public static boolean isSyncable(Account a) {
return 1 == ContentResolver.getIsSyncable(a, BrowserContract.AUTHORITY);
}
public static boolean willEnableOnUpgrade(Account a, AccountManager accountManager) {
return "1".equals(accountManager.getUserData(a, Constants.DATA_ENABLE_ON_UPGRADE));
}
private static Account getTestAccount(AccountManager accountManager) {
final String type = SyncConstants.ACCOUNTTYPE_SYNC;
Account[] existing = accountManager.getAccountsByType(type);
for (Account account : existing) {
if (account.name.equals(TEST_USERNAME)) {
return account;
}
}
return null;
}
private void deleteTestAccount() {
final AccountManager accountManager = AccountManager.get(context);
final Account found = getTestAccount(accountManager);
if (found == null) {
return;
}
TestSyncAccounts.deleteAccount(this, accountManager, found);
}
@Override
public void setUp() {
context = getApplicationContext();
final AccountManager accountManager = AccountManager.get(context);
deleteTestAccount();
// Set up and enable Sync accounts.
SyncAccountParameters syncAccountParams = new SyncAccountParameters(context, accountManager, TEST_USERNAME, TEST_PASSWORD, TEST_SYNC_KEY, TEST_SERVER, null, null, null);
final Account account = SyncAccounts.createSyncAccount(syncAccountParams, true);
assertNotNull(account);
assertTrue(syncsAutomatically(account));
assertTrue(isSyncable(account));
}
private static class LeakySyncAdapter extends SyncAdapter {
public LeakySyncAdapter(Context context, boolean autoInitialize, Account account) {
super(context, autoInitialize);
this.localAccount = account;
}
}
/**
* Verify that when SyncAdapter is informed of an Upgrade Required
* response, that it disables the account it's syncing.
*/
public void testInformUpgradeRequired() {
final AccountManager accountManager = AccountManager.get(context);
final Account account = getTestAccount(accountManager);
assertNotNull(account);
assertTrue(syncsAutomatically(account));
assertTrue(isSyncable(account));
assertFalse(willEnableOnUpgrade(account, accountManager));
LeakySyncAdapter adapter = new LeakySyncAdapter(context, true, account);
adapter.informUpgradeRequiredResponse(null);
// Oh god.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// We have disabled the account, but it's still syncable.
assertFalse(syncsAutomatically(account));
assertTrue(isSyncable(account));
assertTrue(willEnableOnUpgrade(account, accountManager));
}
private class Result {
public boolean called = false;
}
public static HttpResponse simulate400() {
HttpResponse response = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), 400, "Bad Request") {
@Override
public HttpEntity getEntity() {
try {
return new StringEntity("16");
} catch (UnsupportedEncodingException e) {
// Never happens.
return null;
}
}
};
return response;
}
/**
* Verify that when a 400 response is received with an
* "Upgrade Required" response code body, we call
* informUpgradeRequiredResponse on the delegate.
*/
public void testUpgradeResponse() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
final Result calledUpgradeRequired = new Result();
final GlobalSessionCallback callback = new BlankGlobalSessionCallback() {
@Override
public void informUpgradeRequiredResponse(final GlobalSession session) {
calledUpgradeRequired.called = true;
}
};
final GlobalSession session = new MockGlobalSession(
TEST_SERVER, TEST_USERNAME, TEST_PASSWORD,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback);
session.interpretHTTPFailure(simulate400());
assertTrue(calledUpgradeRequired.called);
}
@Override
public void tearDown() {
deleteTestAccount();
}
public abstract class BlankGlobalSessionCallback implements GlobalSessionCallback {
public BlankGlobalSessionCallback() {
}
@Override
public void requestBackoff(long backoff) {
}
@Override
public boolean wantNodeAssignment() {
return false;
}
@Override
public void informUnauthorizedResponse(GlobalSession globalSession,
URI oldClusterURL) {
}
@Override
public void informNodeAssigned(GlobalSession globalSession,
URI oldClusterURL, URI newClusterURL) {
}
@Override
public void informNodeAuthenticationFailed(GlobalSession globalSession,
URI failedClusterURL) {
}
@Override
public void handleAborted(GlobalSession globalSession, String reason) {
}
@Override
public void handleError(GlobalSession globalSession, Exception ex) {
}
@Override
public void handleSuccess(GlobalSession globalSession) {
}
@Override
public void handleStageCompleted(Stage currentState,
GlobalSession globalSession) {
}
@Override
public boolean shouldBackOff() {
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,49 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync;
import java.util.Arrays;
import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
/**
* These tests are on device because the WebKit APIs are stubs on desktop.
*/
public class TestWebURLFinder extends AndroidSyncTestCase {
public String find(String string) {
return new WebURLFinder(string).bestWebURL();
}
public String find(String[] strings) {
return new WebURLFinder(Arrays.asList(strings)).bestWebURL();
}
public void testNoEmail() {
assertNull(find("test@test.com"));
}
public void testSchemeFirst() {
assertEquals("http://scheme.com", find("test.com http://scheme.com"));
}
public void testFullURL() {
assertEquals("http://scheme.com:8080/inner#anchor&arg=1", find("test.com http://scheme.com:8080/inner#anchor&arg=1"));
}
public void testNoScheme() {
assertEquals("noscheme.com", find("noscheme.com"));
}
public void testNoBadScheme() {
assertNull(find("file:///test javascript:///test.js"));
}
public void testStrings() {
assertEquals("http://test.com", find(new String[] { "http://test.com", "noscheme.com" }));
assertEquals("http://test.com", find(new String[] { "noschemefirst.com", "http://test.com" }));
assertEquals("http://test.com/inner#test", find(new String[] { "noschemefirst.com", "http://test.com/inner#test", "http://second.org/fark" }));
assertEquals("http://test.com", find(new String[] { "javascript:///test.js", "http://test.com" }));
}
}

Просмотреть файл

@ -0,0 +1,216 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync.helpers;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
public class BookmarkHelpers {
private static String mobileFolderGuid = "mobile";
private static String mobileFolderName = "mobile";
private static String topFolderGuid = Utils.generateGuid();
private static String topFolderName = "My Top Folder";
private static String middleFolderGuid = Utils.generateGuid();
private static String middleFolderName = "My Middle Folder";
private static String bottomFolderGuid = Utils.generateGuid();
private static String bottomFolderName = "My Bottom Folder";
private static String bmk1Guid = Utils.generateGuid();
private static String bmk2Guid = Utils.generateGuid();
private static String bmk3Guid = Utils.generateGuid();
private static String bmk4Guid = Utils.generateGuid();
/*
* Helpers for creating bookmark records of different types
*/
public static BookmarkRecord createBookmarkInMobileFolder1() {
BookmarkRecord rec = createBookmark1();
rec.guid = Utils.generateGuid();
rec.parentID = mobileFolderGuid;
rec.parentName = mobileFolderName;
return rec;
}
public static BookmarkRecord createBookmarkInMobileFolder2() {
BookmarkRecord rec = createBookmark2();
rec.guid = Utils.generateGuid();
rec.parentID = mobileFolderGuid;
rec.parentName = mobileFolderName;
return rec;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createBookmark1() {
BookmarkRecord record = new BookmarkRecord();
JSONArray tags = new JSONArray();
tags.add("tag1");
tags.add("tag2");
tags.add("tag3");
record.guid = bmk1Guid;
record.title = "Foo!!!";
record.bookmarkURI = "http://foo.bar.com";
record.description = "This is a description for foo.bar.com";
record.tags = tags;
record.keyword = "fooooozzzzz";
record.parentID = topFolderGuid;
record.parentName = topFolderName;
record.type = "bookmark";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createBookmark2() {
BookmarkRecord record = new BookmarkRecord();
JSONArray tags = new JSONArray();
tags.add("tag1");
tags.add("tag2");
record.guid = bmk2Guid;
record.title = "Bar???";
record.bookmarkURI = "http://bar.foo.com";
record.description = "This is a description for Bar???";
record.tags = tags;
record.keyword = "keywordzzz";
record.parentID = topFolderGuid;
record.parentName = topFolderName;
record.type = "bookmark";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createBookmark3() {
BookmarkRecord record = new BookmarkRecord();
JSONArray tags = new JSONArray();
tags.add("tag1");
tags.add("tag2");
record.guid = bmk3Guid;
record.title = "Bmk3";
record.bookmarkURI = "http://bmk3.com";
record.description = "This is a description for bmk3";
record.tags = tags;
record.keyword = "snooozzz";
record.parentID = middleFolderGuid;
record.parentName = middleFolderName;
record.type = "bookmark";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createBookmark4() {
BookmarkRecord record = new BookmarkRecord();
JSONArray tags = new JSONArray();
tags.add("tag1");
tags.add("tag2");
record.guid = bmk4Guid;
record.title = "Bmk4";
record.bookmarkURI = "http://bmk4.com";
record.description = "This is a description for bmk4?";
record.tags = tags;
record.keyword = "booooozzz";
record.parentID = bottomFolderGuid;
record.parentName = bottomFolderName;
record.type = "bookmark";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createMicrosummary() {
BookmarkRecord record = new BookmarkRecord();
JSONArray tags = new JSONArray();
tags.add("tag1");
tags.add("tag2");
record.guid = Utils.generateGuid();
record.title = "Microsummary 1";
record.bookmarkURI = "www.bmkuri.com";
record.description = "microsummary description";
record.tags = tags;
record.keyword = "keywordzzz";
record.parentID = topFolderGuid;
record.parentName = topFolderName;
record.type = "microsummary";
return record;
}
public static BookmarkRecord createQuery() {
BookmarkRecord record = new BookmarkRecord();
record.guid = Utils.generateGuid();
record.title = "Query 1";
record.bookmarkURI = "http://www.query.com";
record.description = "Query 1 description";
record.tags = new JSONArray();
record.keyword = "queryKeyword";
record.parentID = topFolderGuid;
record.parentName = topFolderName;
record.type = "query";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createFolder1() {
BookmarkRecord record = new BookmarkRecord();
record.guid = topFolderGuid;
record.title = topFolderName;
record.parentID = "mobile";
record.parentName = "mobile";
JSONArray children = new JSONArray();
children.add(bmk1Guid);
children.add(bmk2Guid);
record.children = children;
record.type = "folder";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createFolder2() {
BookmarkRecord record = new BookmarkRecord();
record.guid = middleFolderGuid;
record.title = middleFolderName;
record.parentID = topFolderGuid;
record.parentName = topFolderName;
JSONArray children = new JSONArray();
children.add(bmk3Guid);
record.children = children;
record.type = "folder";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createFolder3() {
BookmarkRecord record = new BookmarkRecord();
record.guid = bottomFolderGuid;
record.title = bottomFolderName;
record.parentID = middleFolderGuid;
record.parentName = middleFolderName;
JSONArray children = new JSONArray();
children.add(bmk4Guid);
record.children = children;
record.type = "folder";
return record;
}
@SuppressWarnings("unchecked")
public static BookmarkRecord createLivemark() {
BookmarkRecord record = new BookmarkRecord();
record.guid = Utils.generateGuid();
record.title = "Livemark title";
record.parentID = topFolderGuid;
record.parentName = topFolderName;
JSONArray children = new JSONArray();
children.add(Utils.generateGuid());
children.add(Utils.generateGuid());
record.children = children;
record.type = "livemark";
return record;
}
public static BookmarkRecord createSeparator() {
BookmarkRecord record = new BookmarkRecord();
record.guid = Utils.generateGuid();
record.androidPosition = 3;
record.parentID = topFolderGuid;
record.parentName = topFolderName;
record.type = "separator";
return record;
}
}

Просмотреть файл

@ -0,0 +1,33 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync.helpers;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
public class DefaultBeginDelegate extends DefaultDelegate implements RepositorySessionBeginDelegate {
@Override
public void onBeginFailed(Exception ex) {
performNotify("Begin failed", ex);
}
@Override
public void onBeginSucceeded(RepositorySession session) {
performNotify("Default begin delegate hit.", null);
}
@Override
public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
DefaultBeginDelegate copy;
try {
copy = (DefaultBeginDelegate) this.clone();
copy.executor = executor;
return copy;
} catch (CloneNotSupportedException e) {
return this;
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync.helpers;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate;
public class DefaultCleanDelegate extends DefaultDelegate implements RepositorySessionCleanDelegate {
@Override
public void onCleaned(Repository repo) {
performNotify("Default begin delegate hit.", null);
}
@Override
public void onCleanFailed(Repository repo, Exception ex) {
performNotify("Clean failed.", ex);
}
}

Просмотреть файл

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync.helpers;
import java.util.concurrent.ExecutorService;
import junit.framework.AssertionFailedError;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
public abstract class DefaultDelegate {
protected ExecutorService executor;
protected final WaitHelper waitHelper;
public DefaultDelegate() {
waitHelper = WaitHelper.getTestWaiter();
}
public DefaultDelegate(WaitHelper waitHelper) {
this.waitHelper = waitHelper;
}
protected WaitHelper getTestWaiter() {
return waitHelper;
}
public void performWait(Runnable runnable) throws AssertionFailedError {
getTestWaiter().performWait(runnable);
}
public void performNotify() {
getTestWaiter().performNotify();
}
public void performNotify(Throwable e) {
getTestWaiter().performNotify(e);
}
public void performNotify(String reason, Throwable e) {
String message = reason;
if (e != null) {
message += ": " + e.getMessage();
}
AssertionFailedError ex = new AssertionFailedError(message);
if (e != null) {
ex.initCause(e);
}
getTestWaiter().performNotify(ex);
}
}

Просмотреть файл

@ -0,0 +1,106 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.sync.helpers;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import junit.framework.AssertionFailedError;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.domain.Record;
public class DefaultFetchDelegate extends DefaultDelegate implements RepositorySessionFetchRecordsDelegate {
private static final String LOG_TAG = "DefaultFetchDelegate";
public ArrayList<Record> records = new ArrayList<Record>();
public Set<String> ignore = new HashSet<String>();
@Override
public void onFetchFailed(Exception ex, Record record) {
performNotify("Fetch failed.", ex);
}
protected void onDone(ArrayList<Record> records, HashMap<String, Record> expected, long end) {
Logger.debug(LOG_TAG, "onDone.");
Logger.debug(LOG_TAG, "End timestamp is " + end);
Logger.debug(LOG_TAG, "Expected is " + expected);
Logger.debug(LOG_TAG, "Records is " + records);
Set<String> foundGuids = new HashSet<String>();
try {
int expectedCount = 0;
int expectedFound = 0;
Logger.debug(LOG_TAG, "Counting expected keys.");
for (String key : expected.keySet()) {
if (!ignore.contains(key)) {
expectedCount++;
}
}
Logger.debug(LOG_TAG, "Expected keys: " + expectedCount);
for (Record record : records) {
Logger.debug(LOG_TAG, "Record.");
Logger.debug(LOG_TAG, record.guid);
// Ignore special GUIDs (e.g., for bookmarks).
if (!ignore.contains(record.guid)) {
if (foundGuids.contains(record.guid)) {
fail("Found duplicate guid " + record.guid);
}
Record expect = expected.get(record.guid);
if (expect == null) {
fail("Do not expect to get back a record with guid: " + record.guid); // Caught below
}
Logger.debug(LOG_TAG, "Checking equality.");
try {
assertTrue(expect.equalPayloads(record)); // Caught below
} catch (Exception e) {
Logger.error(LOG_TAG, "ONOZ!", e);
}
Logger.debug(LOG_TAG, "Checked equality.");
expectedFound += 1;
// Track record once we've found it.
foundGuids.add(record.guid);
}
}
assertEquals(expectedCount, expectedFound); // Caught below
Logger.debug(LOG_TAG, "Notifying success.");
performNotify();
} catch (AssertionFailedError e) {
Logger.error(LOG_TAG, "Notifying assertion failure.");
performNotify(e);
} catch (Exception e) {
Logger.error(LOG_TAG, "No!");
performNotify();
}
}
public int recordCount() {
return (this.records == null) ? 0 : this.records.size();
}
@Override
public void onFetchedRecord(Record record) {
Logger.debug(LOG_TAG, "onFetchedRecord(" + record.guid + ")");
records.add(record);
}
@Override
public void onFetchCompleted(final long fetchEnd) {
Logger.debug(LOG_TAG, "onFetchCompleted. Doing nothing.");
}
@Override
public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(final ExecutorService executor) {
return new DeferredRepositorySessionFetchRecordsDelegate(this, executor);
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше