/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sdklib.internal.repository;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.AndroidLocationTestCase;
import com.android.sdklib.internal.repository.DownloadCache.Strategy;
import com.android.sdklib.io.FileOp;
import com.android.sdklib.io.IFileOp;
import com.android.sdklib.io.MockFileOp;
import com.android.utils.Pair;
import com.google.common.base.Charsets;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class DownloadCacheTest extends AndroidLocationTestCase {
private MockFileOp mFileOp;
private MockMonitor mMonitor;
/**
* A private version of DownloadCache that never calls {@link UrlOpener}.
*/
private static class NoDownloadCache extends DownloadCache {
private final Map<String, Pair<InputStream, HttpResponse>> mReplies =
new HashMap<String, Pair<InputStream,HttpResponse>>();
public NoDownloadCache(@NonNull Strategy strategy) {
super(strategy);
}
public NoDownloadCache(@NonNull IFileOp fileOp, @NonNull Strategy strategy) {
super(fileOp, strategy);
}
@Override
protected Pair<InputStream, HttpResponse> openUrl(
@NonNull String url,
boolean needsMarkResetSupport,
@NonNull ITaskMonitor monitor,
@Nullable Header[] headers) throws IOException, CanceledByUserException {
Pair<InputStream, HttpResponse> reply = mReplies.get(url);
if (reply != null) {
return reply;
}
// http-client's behavior is to return a FNF instead of 404.
throw new FileNotFoundException(url);
}
public void registerResponse(@NonNull String url, int httpCode, @Nullable String content) {
InputStream is = null;
if (content != null) {
is = new ByteArrayInputStream(content.getBytes(Charsets.UTF_8));
}
ProtocolVersion p = new ProtocolVersion("HTTP", 1, 1);
StatusLine statusLine = new BasicStatusLine(p, httpCode, "Code " + httpCode);
HttpResponse httpResponse = new BasicHttpResponse(statusLine);
Pair<InputStream, HttpResponse> reply = Pair.of(is, httpResponse);
mReplies.put(url, reply);
}
}
@Override
public void setUp() throws Exception {
super.setUp();
mFileOp = new MockFileOp();
mMonitor = new MockMonitor();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
public void testMissingResource() throws Exception {
// Downloads must fail when using the only-cache strategy and there's nothing in the cache.
// In that case, it returns null to indicate the resource is simply not found.
// Since the mock implementation always returns a 404 and no stream, there is no
// difference between the various cache strategies.
mFileOp.reset();
NoDownloadCache d1 = new NoDownloadCache(mFileOp, Strategy.ONLY_CACHE);
InputStream is1 = d1.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNull(is1);
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d1.getCacheRoot()));
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
// HTTP-Client's behavior is to return a FNF instead of 404 so we'll try that first
mFileOp.reset();
NoDownloadCache d2 = new NoDownloadCache(mFileOp, Strategy.DIRECT);
try {
d2.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
fail("Expected: NoDownloadCache.openCachedUrl should have thrown a FileNotFoundException");
} catch (FileNotFoundException e) {
assertEquals("http://www.example.com/download1.xml", e.getMessage());
}
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
// Try again but this time we'll define a 404 reply to test the rest of the code path.
mFileOp.reset();
d2.registerResponse("http://www.example.com/download1.xml", 404, null);
InputStream is2 = d2.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNull(is2);
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
mFileOp.reset();
NoDownloadCache d3 = new NoDownloadCache(mFileOp, Strategy.SERVE_CACHE);
d3.registerResponse("http://www.example.com/download1.xml", 404, null);
InputStream is3 = d3.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNull(is3);
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
mFileOp.reset();
NoDownloadCache d4 = new NoDownloadCache(mFileOp, Strategy.FRESH_CACHE);
d4.registerResponse("http://www.example.com/download1.xml", 404, null);
InputStream is4 = d4.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNull(is4);
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
}
public void testExistingResource() throws Exception {
// The resource exists but only-cache doesn't hit the network so it will
// fail when the resource is not cached.
mFileOp.reset();
NoDownloadCache d1 = new NoDownloadCache(mFileOp, Strategy.ONLY_CACHE);
d1.registerResponse("http://www.example.com/download1.xml", 200, "Blah blah blah");
InputStream is1 = d1.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNull(is1);
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d1.getCacheRoot()));
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
// HTTP-Client's behavior is to return a FNF instead of 404 so we'll try that first
mFileOp.reset();
NoDownloadCache d2 = new NoDownloadCache(mFileOp, Strategy.DIRECT);
d2.registerResponse("http://www.example.com/download1.xml", 200, "Blah blah blah");
InputStream is2 = d2.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNotNull(is2);
assertEquals("Blah blah blah", new BufferedReader(new InputStreamReader(is2, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals("[]", Arrays.toString(mFileOp.getOutputStreams()));
mFileOp.reset();
NoDownloadCache d3 = new NoDownloadCache(mFileOp, Strategy.SERVE_CACHE);
d3.registerResponse("http://www.example.com/download1.xml", 200, "Blah blah blah");
InputStream is3 = d3.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNotNull(is3);
assertEquals("Blah blah blah", new BufferedReader(new InputStreamReader(is3, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals(
"[<$CACHE/sdkbin-1_9b8dc757-download1_xml: 'Blah blah blah'>, " +
"<$CACHE/sdkinf-1_9b8dc757-download1_xml: '### Meta data for SDK Manager cache. Do not modify.\n" +
"#<creation timestamp>\n" +
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n" +
"'>]",
sanitize(d3, Arrays.toString(mFileOp.getOutputStreams())));
mFileOp.reset();
NoDownloadCache d4 = new NoDownloadCache(mFileOp, Strategy.FRESH_CACHE);
d4.registerResponse("http://www.example.com/download1.xml", 200, "Blah blah blah");
InputStream is4 = d4.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
assertNotNull(is4);
assertEquals("Blah blah blah", new BufferedReader(new InputStreamReader(is4, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertEquals(
"[<$CACHE/sdkbin-1_9b8dc757-download1_xml: 'Blah blah blah'>, " +
"<$CACHE/sdkinf-1_9b8dc757-download1_xml: '### Meta data for SDK Manager cache. Do not modify.\n" +
"#<creation timestamp>\n" +
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n" +
"'>]",
sanitize(d4, Arrays.toString(mFileOp.getOutputStreams())));
}
public void testCachedResource() throws Exception {
mFileOp.reset();
NoDownloadCache d1 = new NoDownloadCache(mFileOp, Strategy.ONLY_CACHE);
d1.registerResponse("http://www.example.com/download1.xml", 200, "This is the new content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d1.getCacheRoot(), "sdkbin-1_9b8dc757-download1_xml")),
123456L,
"This is the cached content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d1.getCacheRoot(), "sdkinf-1_9b8dc757-download1_xml")),
123456L,
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n");
InputStream is1 = d1.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
// Only-cache strategy returns the value from the cache, not the actual resource.
assertEquals("This is the cached content", new BufferedReader(new InputStreamReader(is1, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d1.getCacheRoot()));
// The cache hasn't been modified, only read
assertEquals("[]", sanitize(d1, Arrays.toString(mFileOp.getOutputStreams())));
// Direct ignores the cache.
mFileOp.reset();
NoDownloadCache d2 = new NoDownloadCache(mFileOp, Strategy.DIRECT);
d2.registerResponse("http://www.example.com/download1.xml", 200, "This is the new content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d2.getCacheRoot(), "sdkbin-1_9b8dc757-download1_xml")),
123456L,
"This is the cached content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d2.getCacheRoot(), "sdkinf-1_9b8dc757-download1_xml")),
123456L,
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n");
InputStream is2 = d2.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
// Direct strategy ignores the cache.
assertEquals("This is the new content", new BufferedReader(new InputStreamReader(is2, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d2.getCacheRoot()));
// Direct strategy doesn't update the cache.
assertEquals("[]", sanitize(d2, Arrays.toString(mFileOp.getOutputStreams())));
// Serve-cache reads from the cache if available, ignoring its freshness (here the timestamp
// is way older than the 10-minute freshness encoded in the DownloadCache.)
mFileOp.reset();
NoDownloadCache d3 = new NoDownloadCache(mFileOp, Strategy.SERVE_CACHE);
d3.registerResponse("http://www.example.com/download1.xml", 200, "This is the new content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d3.getCacheRoot(), "sdkbin-1_9b8dc757-download1_xml")),
123456L,
"This is the cached content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d3.getCacheRoot(), "sdkinf-1_9b8dc757-download1_xml")),
123456L,
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n");
InputStream is3 = d3.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
// We get content from the cache.
assertEquals("This is the cached content", new BufferedReader(new InputStreamReader(is3, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d3.getCacheRoot()));
// Cache isn't updated since nothing fresh was read.
assertEquals("[]", sanitize(d3, Arrays.toString(mFileOp.getOutputStreams())));
// fresh-cache reads the cache, finds it stale (here the timestamp
// is way older than the 10-minute freshness encoded in the DownloadCache)
// and will fetch the new resource instead and update the cache.
mFileOp.reset();
NoDownloadCache d4 = new NoDownloadCache(mFileOp, Strategy.FRESH_CACHE);
d4.registerResponse("http://www.example.com/download1.xml", 200, "This is the new content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d4.getCacheRoot(), "sdkbin-1_9b8dc757-download1_xml")),
123456L,
"This is the cached content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d4.getCacheRoot(), "sdkinf-1_9b8dc757-download1_xml")),
123456L,
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n");
InputStream is4 = d4.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
// Cache is discarded, actual resource is returned.
assertEquals("This is the new content", new BufferedReader(new InputStreamReader(is4, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d4.getCacheRoot()));
// Cache isn updated since something fresh was read.
assertEquals(
"[<$CACHE/sdkbin-1_9b8dc757-download1_xml: 'This is the new content'>, " +
"<$CACHE/sdkinf-1_9b8dc757-download1_xml: '### Meta data for SDK Manager cache. Do not modify.\n" +
"#<creation timestamp>\n" +
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n" +
"'>]",
sanitize(d4, Arrays.toString(mFileOp.getOutputStreams())));
// fresh-cache reads the cache, finds it still valid stale (less than 10-minute old),
// and uses the cached resource.
mFileOp.reset();
NoDownloadCache d5 = new NoDownloadCache(mFileOp, Strategy.FRESH_CACHE);
d5.registerResponse("http://www.example.com/download1.xml", 200, "This is the new content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d5.getCacheRoot(), "sdkbin-1_9b8dc757-download1_xml")),
System.currentTimeMillis() - 1000,
"This is the cached content");
mFileOp.recordExistingFile(
mFileOp.getAgnosticAbsPath(FileOp.append(d5.getCacheRoot(), "sdkinf-1_9b8dc757-download1_xml")),
System.currentTimeMillis() - 1000,
"URL=http\\://www.example.com/download1.xml\n" +
"Status-Code=200\n");
InputStream is5 = d5.openCachedUrl("http://www.example.com/download1.xml", mMonitor);
// Cache is used.
assertEquals("This is the cached content", new BufferedReader(new InputStreamReader(is5, Charsets.UTF_8)).readLine());
assertEquals("", mMonitor.getAllCapturedLogs());
assertTrue(mFileOp.hasRecordedExistingFolder(d5.getCacheRoot()));
// Cache isn't updated since nothing fresh was read.
assertEquals("[]", sanitize(d5, Arrays.toString(mFileOp.getOutputStreams())));
}
// --------
@Nullable
private String sanitize(@NonNull DownloadCache dc, @Nullable String msg) {
if (msg != null) {
msg = msg.replace("\r\n", "\n");
String absRoot = mFileOp.getAgnosticAbsPath(dc.getCacheRoot());
msg = msg.replace(absRoot, "$CACHE");
// Cached files also contain a creation timestamp which we need to find and remove.
msg = msg.replaceAll("\n#[A-Z][A-Za-z0-9: ]+20[0-9]{2}\n", "\n#<creation timestamp>\n");
}
return msg;
}
}