зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to inbound.
This commit is contained in:
Коммит
d770b2ae6a
|
@ -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
|
||||
===============
|
||||
|
|
|
@ -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)
|
||||
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 7.7 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.2 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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);
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче