/*
* Copyright 2010 Google Inc.
*
* 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.google.jstestdriver;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.google.jstestdriver.JsonCommand.CommandType;
import com.google.jstestdriver.Response.ResponseType;
import com.google.jstestdriver.browser.BrowserFileSet;
import com.google.jstestdriver.hooks.FileInfoScheme;
import com.google.jstestdriver.model.HandlerPathPrefix;
import com.google.jstestdriver.model.JstdTestCase;
import com.google.jstestdriver.model.JstdTestCaseDelta;
import com.google.jstestdriver.servlet.fileset.BrowserFileCheck;
import com.google.jstestdriver.servlet.fileset.DeltaUpload;
import com.google.jstestdriver.servlet.fileset.TestCaseUpload;
import com.google.jstestdriver.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Handles the uploading of files.
*
* @author corysmith@google.com (Cory Smith)
*/
public class FileUploader {
public static final int CHUNK_SIZE = 50;
private final StopWatch stopWatch;
private final Gson gson = new Gson();
private final Server server;
private final String baseUrl;
private final FileLoader fileLoader;
private final JsTestDriverFileFilter filter;
private final Set<FileInfoScheme> schemes;
private static final Logger logger = LoggerFactory.getLogger(FileUploader.class);
private final HandlerPathPrefix prefix;
@Inject
public FileUploader(StopWatch stopWatch, Server server,
@Named("server") String baseUrl, FileLoader fileLoader,
JsTestDriverFileFilter filter,
Set<FileInfoScheme> schemes,
@Named("serverHandlerPrefix") HandlerPathPrefix prefix) {
this.stopWatch = stopWatch;
this.server = server;
this.baseUrl = baseUrl;
this.fileLoader = fileLoader;
this.filter = filter;
this.schemes = schemes;
this.prefix = prefix;
}
/** Uploads the changed files to the server and the browser. */
public void uploadFileSet(String browserId, Collection<JstdTestCase> testCases, ResponseStream stream) {
stopWatch.start("determineServerFileSet(%s)", browserId);
final Collection<JstdTestCaseDelta> deltas = determineServerFileSet(testCases);
stopWatch.stop("determineServerFileSet(%s)", browserId);
logger.debug("Deltas: {}", deltas);
stopWatch.start("upload to server %s", browserId);
uploadToServer(deltas);
stopWatch.stop("upload to server %s", browserId);
for (JstdTestCase testCase : testCases) {
stopWatch.start("determineBrowserFileSet(%s)", browserId);
final List<FileInfo> browserFilesToUpdate = determineBrowserFileSet(browserId, testCase, stream);
stopWatch.stop("determineBrowserFileSet(%s)", browserId);
stopWatch.start("uploadToTheBrowser(%s)", browserId);
uploadToTheBrowser(browserId, stream, browserFilesToUpdate, CHUNK_SIZE);
stopWatch.stop("uploadToTheBrowser(%s)", browserId);
}
}
/**
* Uploads the {@link JstdTestCase}s to the server, and retrieves a list
* of {@link JstdTestCaseDelta}s of the files that are different.
*/
public Collection<JstdTestCaseDelta> determineServerFileSet(Collection<JstdTestCase> testCases) {
Map<String, String> fileSetParams = new LinkedHashMap<String, String>();
fileSetParams.put("data", gson.toJson(testCases));
fileSetParams.put("action", TestCaseUpload.ACTION);
String postResult = server.post(baseUrl + "/fileSet", fileSetParams);
return gson.fromJson(postResult, new TypeToken<Collection<JstdTestCaseDelta>>() {}.getType());
}
/** Determines what files have been changed as compared to the server. */
public List<FileInfo> determineBrowserFileSet(String browserId, JstdTestCase testCase,
ResponseStream stream) {
BrowserFileSet browserFileSet = getBrowserFileSet(browserId, testCase);
stopWatch.start("resolving browser upload %s", browserId);
try {
logger.debug("Updating files {}", browserFileSet.getFilesToUpload());
// need a linked hashset here to avoid adding a file more than once.
final Set<FileInfo> finalFilesToUpload = new LinkedHashSet<FileInfo>();
// reset if there are extra files in the browser
if (browserFileSet.shouldReset() || !browserFileSet.getExtraFiles().isEmpty()) {
reset(browserId, stream, testCase);
browserFileSet = getBrowserFileSet(browserId, testCase);
logger.info("second fileset {}", browserFileSet);
}
for (FileInfo file : browserFileSet.getFilesToUpload()) {
finalFilesToUpload.addAll(determineInBrowserDependencies(file, testCase.getServable()));
}
return Lists.newArrayList(finalFilesToUpload);
} finally {
stopWatch.stop("resolving browser upload %s", browserId);
}
}
private BrowserFileSet getBrowserFileSet(String browserId, JstdTestCase testCase) {
stopWatch.start("get upload set %s", browserId);
Map<String, String> fileSetParams = new LinkedHashMap<String, String>();
fileSetParams.put("id", browserId);
fileSetParams.put("data", gson.toJson(testCase));
fileSetParams.put("action", BrowserFileCheck.ACTION);
//logger.info("FileParams: {}", fileSetParams);
String postResult = server.post(baseUrl + "/fileSet", fileSetParams);
if (postResult.length() < 0) {
return new BrowserFileSet(Collections.<FileInfo>emptyList(), Collections.<FileInfo>emptyList(), false);
}
BrowserFileSet browserFileSet = gson.fromJson(postResult, BrowserFileSet.class);
stopWatch.stop("get upload set %s", browserId);
return browserFileSet;
}
/** Uploads files to the browser. */
public void uploadToTheBrowser(String browserId, ResponseStream stream,
List<FileInfo> loadedFiles, int chunkSize) {
List<FileSource> filesSrc = Lists.newLinkedList(filterFilesToLoad(loadedFiles));
int numberOfFilesToLoad = filesSrc.size();
logger.info("Files toupload {}",
Lists.transform(Lists.newArrayList(loadedFiles), new Function<FileInfo, String>() {
@Override
public String apply(FileInfo in) {
return "\n" + in.getDisplayPath();
}
}));
for (int i = 0; i < numberOfFilesToLoad; i += chunkSize) {
int chunkEndIndex = Math.min(i + chunkSize, numberOfFilesToLoad);
List<String> loadParameters = new LinkedList<String>();
List<FileSource> filesToLoad = filesSrc.subList(i, chunkEndIndex);
loadParameters.add(gson.toJson(filesToLoad));
loadParameters.add("false");
JsonCommand cmd = new JsonCommand(CommandType.LOADTEST, loadParameters);
Map<String, String> loadFileParams = new LinkedHashMap<String, String>();
loadFileParams.put("id", browserId);
loadFileParams.put("data", gson.toJson(cmd));
if (logger.isDebugEnabled()) {
logger.debug("Sending LOADTEST to {} for {}", browserId,
Lists.transform(filesToLoad, new Function<FileSource, String>() {
@Override
public String apply(FileSource in) {
return "\n" + in.getFileSrc();
}
}));
}
server.post(baseUrl + "/cmd", loadFileParams);
while (true) {
String jsonResponse = server.fetch(baseUrl + "/cmd?id=" + browserId);
StreamMessage message = gson.fromJson(jsonResponse, StreamMessage.class);
Response response = message.getResponse();
logger.trace("LOADTEST response for {}", response);
stream.stream(response);
if (message.isLast()) {
logger.debug("Finished LOADTEST on {} with {}", browserId, response.getResponseType());
break;
}
}
}
}
public void uploadToServer(final Collection<JstdTestCaseDelta> deltas) {
if (deltas.isEmpty()) {
return;
}
List<JstdTestCaseDelta> loadedDeltas = Lists.newArrayListWithCapacity(deltas.size());
for (JstdTestCaseDelta delta : deltas) {
loadedDeltas.add(delta.loadFiles(fileLoader));
}
for (List<JstdTestCaseDelta> partition : Lists.partition(loadedDeltas, 50)) {
Map<String, String> uploadFileParams = new LinkedHashMap<String, String>();
uploadFileParams.put("action", DeltaUpload.ACTION);
uploadFileParams.put("data", gson.toJson(partition));
server.post(baseUrl + "/fileSet", uploadFileParams);
}
}
private void reset(String browserId, ResponseStream stream, JstdTestCase testCase) {
stopWatch.start("reset %s", browserId);
JsonCommand cmd = new JsonCommand(CommandType.RESET,
Lists.newArrayList("preload", testCase.getId()));
Map<String, String> resetParams = new LinkedHashMap<String, String>();
logger.debug("reset browser {} testcase {}", browserId, testCase.getId());
resetParams.put("id", browserId);
resetParams.put("data", gson.toJson(cmd));
server.post(baseUrl + "/cmd", resetParams);
logger.trace("starting reset for {}", browserId);
Response response;
StreamMessage message;
do {
String jsonResponse = server.fetch(baseUrl + "/cmd?id=" + browserId);
message = gson.fromJson(jsonResponse, StreamMessage.class);
response = message.getResponse();
stream.stream(response);
} while(!(ResponseType.RESET_RESULT.equals(response.getResponseType()) && message.isLast()));
logger.trace("finished reset for {}", browserId);
stopWatch.stop("reset %s", browserId);
}
/**
* Determines what files must be reloaded in the browser, based on this file
* being updated.
*/
private Collection<FileInfo> determineInBrowserDependencies(FileInfo file, List<FileInfo> files) {
LinkedHashSet<FileInfo> deps = Sets.newLinkedHashSet();
for (FileInfo dep : filter.resolveFilesDeps(file, files)) {
deps.add(dep);
}
return deps;
}
private List<FileSource> filterFilesToLoad(Collection<FileInfo> fileInfos) {
List<FileSource> filteredFileSources = new LinkedList<FileSource>();
for (FileInfo fileInfo : fileInfos) {
if (!fileInfo.isServeOnly()) {
filteredFileSources.add(fileInfo.toFileSource(prefix, schemes));
}
}
return filteredFileSources;
}
}