<commit_before>/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, PathUtils, CodeMirror */

/**
 * Set of utilities for simple parsing of JS text.
 */
define(function (require, exports, module) {
    "use strict";

    // Load brackets modules
    var Async                   = require("utils/Async"),
        DocumentManager         = require("document/DocumentManager"),
        ChangedDocumentTracker  = require("document/ChangedDocumentTracker"),
        NativeFileSystem        = require("file/NativeFileSystem").NativeFileSystem,
        PerfUtils               = require("utils/PerfUtils"),
        StringUtils             = require("utils/StringUtils");

    /**
     * Tracks dirty documents between invocations of findMatchingFunctions.
     * @type {ChangedDocumentTracker}
     */
    var _changedDocumentTracker = new ChangedDocumentTracker();

    /**
     * Function matching regular expression. Recognizes the forms:
     * "function functionName()", "functionName = function()", and
     * "functionName: function()".
     *
     * Note: JavaScript identifier matching is not strictly to spec. This
     * RegExp matches any sequence of characters that is not whitespace.
     * @type {RegExp}
     */
    var _functionRegExp = /(function\s+([$_A-Za-z\u007F-\uFFFF][$_A-Za-z0-9\u007F-\uFFFF]*)\s*(\([^)]*\)))|(([$_A-Za-z\u007F-\uFFFF][$_A-Za-z0-9\u007F-\uFFFF]*)\s*[:=]\s*function\s*(\([^)]*\)))/g;

    /**
     * @private
     * Return an Array with names and offsets for all functions in the specified text
     * @param {!string} text Document text
     * @return {Object.<string, Array.<{offsetStart: number, offsetEnd: number}>}
     */
    function _findAllFunctionsInText(text) {
        var results = {},
            functionName,
            match;

        PerfUtils.markStart(PerfUtils.JSUTILS_REGEXP);

        while ((match = _functionRegExp.exec(text)) !== null) {
            functionName = (match[2] || match[5]).trim();

            if (!Array.isArray(results[functionName])) {
                results[functionName] = [];
            }

            results[functionName].push({offsetStart: match.index});
        }

        PerfUtils.addMeasurement(PerfUtils.JSUTILS_REGEXP);

        return results;
    }

    // Given the start offset of a function definition (before the opening brace), find
    // the end offset for the function (the closing "}"). Returns the position one past the
    // close brace. Properly ignores braces inside comments, strings, and regexp literals.
    function _getFunctionEndOffset(text, offsetStart) {
        var mode = CodeMirror.getMode({}, "javascript");
        var state = CodeMirror.startState(mode), stream, style, token;
        var curOffset = offsetStart, length = text.length, blockCount = 0, lineStart;
        var foundStartBrace = false;

        // Get a stream for the next line, and update curOffset and lineStart to point to the
        // beginning of that next line. Returns false if we're at the end of the text.
        function nextLine() {
            if (stream) {
                curOffset++; // account for \n
                if (curOffset >= length) {
                    return false;
                }
            }
            lineStart = curOffset;
            var lineEnd = text.indexOf("\n", lineStart);
            if (lineEnd === -1) {
                lineEnd = length;
            }
            stream = new CodeMirror.StringStream(text.slice(curOffset, lineEnd));
            return true;
        }

        // Get the next token, updating the style and token to refer to the current
        // token, and updating the curOffset to point to the end of the token (relative
        // to the start of the original text).
        function nextToken() {
            if (curOffset >= length) {
                return false;
            }
            if (stream) {
                // Set the start of the next token to the current stream position.
                stream.start = stream.pos;
            }
            while (!stream || stream.eol()) {
                if (!nextLine()) {
                    return false;
                }
            }
            style = mode.token(stream, state);
            token = stream.current();
            curOffset = lineStart + stream.pos;
            return true;
        }

        while (nextToken()) {
            if (style !== "comment" && style !== "regexp" && style !== "string") {
                if (token === "{") {
                    foundStartBrace = true;
                    blockCount++;
                } else if (token === "}") {
                    blockCount--;
                }
            }

            // blockCount starts at 0, so we don't want to check if it hits 0
            // again until we've actually gone past the start of the function body.
            if (foundStartBrace && blockCount <= 0) {
                return curOffset;
            }
        }

        // Shouldn't get here, but if we do, return the end of the text as the offset.
        return length;
    }

    /**
     * @private
     * Computes function offsetEnd, lineStart and lineEnd. Appends a result record to rangeResults.
     * @param {!Document} doc
     * @param {!string} functionName
     * @param {!Array.<{offsetStart: number, offsetEnd: number}>} functions
     * @param {!Array.<{document: Document, name: string, lineStart: number, lineEnd: number}>} rangeResults
     */
    function _computeOffsets(doc, functionName, functions, rangeResults) {
        var text    = doc.getText(),
            lines   = StringUtils.getLines(text);

        functions.forEach(function (funcEntry) {
            if (!funcEntry.offsetEnd) {
                PerfUtils.markStart(PerfUtils.JSUTILS_END_OFFSET);

                funcEntry.offsetEnd = _getFunctionEndOffset(text, funcEntry.offsetStart);
                funcEntry.lineStart = StringUtils.offsetToLineNum(lines, funcEntry.offsetStart);
                funcEntry.lineEnd   = StringUtils.offsetToLineNum(lines, funcEntry.offsetEnd);

                PerfUtils.addMeasurement(PerfUtils.JSUTILS_END_OFFSET);
            }

            rangeResults.push({
                document:   doc,
                name:       functionName,
                lineStart:  funcEntry.lineStart,
                lineEnd:    funcEntry.lineEnd
            });
        });
    }

    /**
     * @private
     * Read a file and build a function list. Result is cached in fileInfo.
     * @param {!FileInfo} fileInfo File to parse
     * @param {!$.Deferred} result Deferred to resolve with all functions found and the document
     */
    function _readFile(fileInfo, result) {
        DocumentManager.getDocumentForPath(fileInfo.fullPath)
            .done(function (doc) {
                var allFunctions = _findAllFunctionsInText(doc.getText());

                // Cache the result in the fileInfo object
                fileInfo.JSUtils = {};
                fileInfo.JSUtils.functions = allFunctions;
                fileInfo.JSUtils.timestamp = doc.diskTimestamp;

                result.resolve({doc: doc, functions: allFunctions});
            })
            .fail(function (error) {
                result.reject(error);
            });
    }

    /**
     * Determines if the document function cache is up to date.
     * @param {FileInfo} fileInfo
     * @return {$.Promise} A promise resolved with true with true when a function cache is available for the document. Resolves
     *   with false when there is no cache or the cache is stale.
     */
    function _shouldGetFromCache(fileInfo) {
        var result = new $.Deferred(),
            isChanged = _changedDocumentTracker.isPathChanged(fileInfo.fullPath);

        if (isChanged && fileInfo.JSUtils) {
            // See if it's dirty and in the working set first
            var doc = DocumentManager.getOpenDocumentForPath(fileInfo.fullPath);

            if (doc && doc.isDirty) {
                result.resolve(false);
            } else {
                // If a cache exists, check the timestamp on disk
                var file = new NativeFileSystem.FileEntry(fileInfo.fullPath);

                file.getMetadata(
                    function (metadata) {
                        result.resolve(fileInfo.JSUtils.timestamp === metadata.diskTimestamp);
                    },
                    function (error) {
                        result.reject(error);
                    }
                );
            }
        } else {
            // Use the cache if the file did not change and the cache exists
            result.resolve(!isChanged && fileInfo.JSUtils);
        }

        return result.promise();
    }

    /**
     * @private
     * Compute lineStart and lineEnd for each matched function
     * @param {!Array.<{doc: Document, fileInfo: FileInfo, functions: Array.<offsetStart: number, offsetEnd: number>}>} docEntries
     * @param {!string} functionName
     * @param {!Array.<document: Document, name: string, lineStart: number, lineEnd: number>} rangeResults
     * @return {$.Promise} A promise resolved with an array of document ranges to populate a MultiRangeInlineEditor.
     */
    function _getOffsetsForFunction(docEntries, functionName) {
        // Filter for documents that contain the named function
        var result              = new $.Deferred(),
            matchedDocuments    = [],
            rangeResults        = [],
            functionsInDocument;

        docEntries.forEach(function (docEntry) {
            functionsInDocument = docEntry.functions[functionName];

            if (functionsInDocument) {
                matchedDocuments.push({doc: docEntry.doc, fileInfo: docEntry.fileInfo, functions: functionsInDocument});
            }
        });

        Async.doInParallel(matchedDocuments, function (docEntry) {
            var doc         = docEntry.doc,
                oneResult   = new $.Deferred();

            // doc will be undefined if we hit the cache
            if (!doc) {
                DocumentManager.getDocumentForPath(docEntry.fileInfo.fullPath)
                    .done(function (fetchedDoc) {
                        _computeOffsets(fetchedDoc, functionName, docEntry.functions, rangeResults);
                    })
                    .always(function () {
                        oneResult.resolve();
                    });
            } else {
                _computeOffsets(doc, functionName, docEntry.functions, rangeResults);
                oneResult.resolve();
            }

            return oneResult.promise();
        }).done(function () {
            result.resolve(rangeResults);
        });

        return result.promise();
    }

    /**
     * Resolves with a record containing the Document or FileInfo and an Array of all
     * function names with offsets for the specified file. Results may be cached.
     * @param {FileInfo} fileInfo
     * @return {$.Promise} A promise resolved with a document info object that
     *   contains a map of all function names from the document and each function's start offset.
     */
    function _getFunctionsForFile(fileInfo) {
        var result = new $.Deferred();

        _shouldGetFromCache(fileInfo)
            .done(function (useCache) {
                if (useCache) {
                    // Return cached data. doc property is undefined since we hit the cache.
                    // _getOffsets() will fetch the Document if necessary.
                    result.resolve({/*doc: undefined,*/fileInfo: fileInfo, functions: fileInfo.JSUtils.functions});
                } else {
                    _readFile(fileInfo, result);
                }
            }).fail(function (err) {
                result.reject(err);
            });

        return result.promise();
    }

    /**
     * @private
     * Get all functions for each FileInfo.
     * @param {Array.<FileInfo>} fileInfos
     * @return {$.Promise} A promise resolved with an array of document info objects that each
     *   contain a map of all function names from the document and each function's start offset.
     */
    function _getFunctionsInFiles(fileInfos) {
        var result          = new $.Deferred(),
            docEntries      = [];

        PerfUtils.markStart(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS);

        Async.doInParallel(fileInfos, function (fileInfo) {
            var oneResult = new $.Deferred();

            _getFunctionsForFile(fileInfo)
                .done(function (docInfo) {
                    docEntries.push(docInfo);
                })
                .always(function (error) {
                    // If one file fails, continue to search
                    oneResult.resolve();
                });

            return oneResult.promise();
        }).always(function () {
            // Reset ChangedDocumentTracker now that the cache is up to date.
            _changedDocumentTracker.reset();

            PerfUtils.addMeasurement(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS);
            result.resolve(docEntries);
        });

        return result.promise();
    }

    /**
     * Return all functions that have the specified name.
     *
     * @param {!String} functionName The name to match.
     * @param {!Array.<FileIndexManager.FileInfo>} fileInfos The array of files to search.
     * @return {$.Promise} that will be resolved with an Array of objects containing the
     *      source document, start line, and end line (0-based, inclusive range) for each matching function list.
     *      Does not addRef() the documents returned in the array.
     */
    function findMatchingFunctions(functionName, fileInfos) {
        var result          = new $.Deferred(),
            jsFiles         = [],
            docEntries      = [];

        // Filter fileInfos for .js files
        jsFiles = fileInfos.filter(function (fileInfo) {
            return (/^\.js/i).test(PathUtils.filenameExtension(fileInfo.fullPath));
        });

        // RegExp search (or cache lookup) for all functions in the project
        _getFunctionsInFiles(jsFiles).done(function (docEntries) {
            // Compute offsets for all matched functions
            _getOffsetsForFunction(docEntries, functionName).done(function (rangeResults) {
                result.resolve(rangeResults);
            });
        });

        return result.promise();
    }

    /**
     * Finds all instances of the specified functionName in "text".
     * Returns an Array of Objects with start and end properties.
     *
     * @param text {!String} JS text to search
     * @param functionName {!String} function name to search for
     * @return {Array.<{offset:number, functionName:string}>}
     *      Array of objects containing the start offset for each matched function name.
     */
    function findAllMatchingFunctionsInText(text, functionName) {
        var allFunctions = _findAllFunctionsInText(text);
        var result = [];
        var lines = text.split("\n");

        $.each(allFunctions, function (index, functions) {
            if (index === functionName || functionName === "*") {
                functions.forEach(function (funcEntry) {
                    var endOffset = _getFunctionEndOffset(text, funcEntry.offsetStart);
                    result.push({
                        name: index,
                        lineStart: StringUtils.offsetToLineNum(lines, funcEntry.offsetStart),
                        lineEnd: StringUtils.offsetToLineNum(lines, endOffset)
                    });
                });
            }
        });

        return result;
    }

    PerfUtils.createPerfMeasurement("JSUTILS_GET_ALL_FUNCTIONS", "Parallel file search across project");
    PerfUtils.createPerfMeasurement("JSUTILS_REGEXP", "RegExp search for all functions");
    PerfUtils.createPerfMeasurement("JSUTILS_END_OFFSET", "Find end offset for a single matched function");

    exports.findAllMatchingFunctionsInText = findAllMatchingFunctionsInText;
    exports._getFunctionEndOffset = _getFunctionEndOffset; // For testing only
    exports.findMatchingFunctions = findMatchingFunctions;
});
<commit_msg>Use for..in to iterate over array-like objects<commit_after>/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, PathUtils, CodeMirror */

/**
 * Set of utilities for simple parsing of JS text.
 */
define(function (require, exports, module) {
    "use strict";

    // Load brackets modules
    var Async                   = require("utils/Async"),
        DocumentManager         = require("document/DocumentManager"),
        ChangedDocumentTracker  = require("document/ChangedDocumentTracker"),
        NativeFileSystem        = require("file/NativeFileSystem").NativeFileSystem,
        PerfUtils               = require("utils/PerfUtils"),
        StringUtils             = require("utils/StringUtils");

    /**
     * Tracks dirty documents between invocations of findMatchingFunctions.
     * @type {ChangedDocumentTracker}
     */
    var _changedDocumentTracker = new ChangedDocumentTracker();

    /**
     * Function matching regular expression. Recognizes the forms:
     * "function functionName()", "functionName = function()", and
     * "functionName: function()".
     *
     * Note: JavaScript identifier matching is not strictly to spec. This
     * RegExp matches any sequence of characters that is not whitespace.
     * @type {RegExp}
     */
    var _functionRegExp = /(function\s+([$_A-Za-z\u007F-\uFFFF][$_A-Za-z0-9\u007F-\uFFFF]*)\s*(\([^)]*\)))|(([$_A-Za-z\u007F-\uFFFF][$_A-Za-z0-9\u007F-\uFFFF]*)\s*[:=]\s*function\s*(\([^)]*\)))/g;

    /**
     * @private
     * Return an Array with names and offsets for all functions in the specified text
     * @param {!string} text Document text
     * @return {Object.<string, Array.<{offsetStart: number, offsetEnd: number}>}
     */
    function _findAllFunctionsInText(text) {
        var results = {},
            functionName,
            match;

        PerfUtils.markStart(PerfUtils.JSUTILS_REGEXP);

        while ((match = _functionRegExp.exec(text)) !== null) {
            functionName = (match[2] || match[5]).trim();

            if (!Array.isArray(results[functionName])) {
                results[functionName] = [];
            }

            results[functionName].push({offsetStart: match.index});
        }

        PerfUtils.addMeasurement(PerfUtils.JSUTILS_REGEXP);

        return results;
    }

    // Given the start offset of a function definition (before the opening brace), find
    // the end offset for the function (the closing "}"). Returns the position one past the
    // close brace. Properly ignores braces inside comments, strings, and regexp literals.
    function _getFunctionEndOffset(text, offsetStart) {
        var mode = CodeMirror.getMode({}, "javascript");
        var state = CodeMirror.startState(mode), stream, style, token;
        var curOffset = offsetStart, length = text.length, blockCount = 0, lineStart;
        var foundStartBrace = false;

        // Get a stream for the next line, and update curOffset and lineStart to point to the
        // beginning of that next line. Returns false if we're at the end of the text.
        function nextLine() {
            if (stream) {
                curOffset++; // account for \n
                if (curOffset >= length) {
                    return false;
                }
            }
            lineStart = curOffset;
            var lineEnd = text.indexOf("\n", lineStart);
            if (lineEnd === -1) {
                lineEnd = length;
            }
            stream = new CodeMirror.StringStream(text.slice(curOffset, lineEnd));
            return true;
        }

        // Get the next token, updating the style and token to refer to the current
        // token, and updating the curOffset to point to the end of the token (relative
        // to the start of the original text).
        function nextToken() {
            if (curOffset >= length) {
                return false;
            }
            if (stream) {
                // Set the start of the next token to the current stream position.
                stream.start = stream.pos;
            }
            while (!stream || stream.eol()) {
                if (!nextLine()) {
                    return false;
                }
            }
            style = mode.token(stream, state);
            token = stream.current();
            curOffset = lineStart + stream.pos;
            return true;
        }

        while (nextToken()) {
            if (style !== "comment" && style !== "regexp" && style !== "string") {
                if (token === "{") {
                    foundStartBrace = true;
                    blockCount++;
                } else if (token === "}") {
                    blockCount--;
                }
            }

            // blockCount starts at 0, so we don't want to check if it hits 0
            // again until we've actually gone past the start of the function body.
            if (foundStartBrace && blockCount <= 0) {
                return curOffset;
            }
        }

        // Shouldn't get here, but if we do, return the end of the text as the offset.
        return length;
    }

    /**
     * @private
     * Computes function offsetEnd, lineStart and lineEnd. Appends a result record to rangeResults.
     * @param {!Document} doc
     * @param {!string} functionName
     * @param {!Array.<{offsetStart: number, offsetEnd: number}>} functions
     * @param {!Array.<{document: Document, name: string, lineStart: number, lineEnd: number}>} rangeResults
     */
    function _computeOffsets(doc, functionName, functions, rangeResults) {
        var text    = doc.getText(),
            lines   = StringUtils.getLines(text);

        functions.forEach(function (funcEntry) {
            if (!funcEntry.offsetEnd) {
                PerfUtils.markStart(PerfUtils.JSUTILS_END_OFFSET);

                funcEntry.offsetEnd = _getFunctionEndOffset(text, funcEntry.offsetStart);
                funcEntry.lineStart = StringUtils.offsetToLineNum(lines, funcEntry.offsetStart);
                funcEntry.lineEnd   = StringUtils.offsetToLineNum(lines, funcEntry.offsetEnd);

                PerfUtils.addMeasurement(PerfUtils.JSUTILS_END_OFFSET);
            }

            rangeResults.push({
                document:   doc,
                name:       functionName,
                lineStart:  funcEntry.lineStart,
                lineEnd:    funcEntry.lineEnd
            });
        });
    }

    /**
     * @private
     * Read a file and build a function list. Result is cached in fileInfo.
     * @param {!FileInfo} fileInfo File to parse
     * @param {!$.Deferred} result Deferred to resolve with all functions found and the document
     */
    function _readFile(fileInfo, result) {
        DocumentManager.getDocumentForPath(fileInfo.fullPath)
            .done(function (doc) {
                var allFunctions = _findAllFunctionsInText(doc.getText());

                // Cache the result in the fileInfo object
                fileInfo.JSUtils = {};
                fileInfo.JSUtils.functions = allFunctions;
                fileInfo.JSUtils.timestamp = doc.diskTimestamp;

                result.resolve({doc: doc, functions: allFunctions});
            })
            .fail(function (error) {
                result.reject(error);
            });
    }

    /**
     * Determines if the document function cache is up to date.
     * @param {FileInfo} fileInfo
     * @return {$.Promise} A promise resolved with true with true when a function cache is available for the document. Resolves
     *   with false when there is no cache or the cache is stale.
     */
    function _shouldGetFromCache(fileInfo) {
        var result = new $.Deferred(),
            isChanged = _changedDocumentTracker.isPathChanged(fileInfo.fullPath);

        if (isChanged && fileInfo.JSUtils) {
            // See if it's dirty and in the working set first
            var doc = DocumentManager.getOpenDocumentForPath(fileInfo.fullPath);

            if (doc && doc.isDirty) {
                result.resolve(false);
            } else {
                // If a cache exists, check the timestamp on disk
                var file = new NativeFileSystem.FileEntry(fileInfo.fullPath);

                file.getMetadata(
                    function (metadata) {
                        result.resolve(fileInfo.JSUtils.timestamp === metadata.diskTimestamp);
                    },
                    function (error) {
                        result.reject(error);
                    }
                );
            }
        } else {
            // Use the cache if the file did not change and the cache exists
            result.resolve(!isChanged && fileInfo.JSUtils);
        }

        return result.promise();
    }

    /**
     * @private
     * Compute lineStart and lineEnd for each matched function
     * @param {!Array.<{doc: Document, fileInfo: FileInfo, functions: Array.<offsetStart: number, offsetEnd: number>}>} docEntries
     * @param {!string} functionName
     * @param {!Array.<document: Document, name: string, lineStart: number, lineEnd: number>} rangeResults
     * @return {$.Promise} A promise resolved with an array of document ranges to populate a MultiRangeInlineEditor.
     */
    function _getOffsetsForFunction(docEntries, functionName) {
        // Filter for documents that contain the named function
        var result              = new $.Deferred(),
            matchedDocuments    = [],
            rangeResults        = [],
            functionsInDocument;

        docEntries.forEach(function (docEntry) {
            functionsInDocument = docEntry.functions[functionName];

            if (functionsInDocument) {
                matchedDocuments.push({doc: docEntry.doc, fileInfo: docEntry.fileInfo, functions: functionsInDocument});
            }
        });

        Async.doInParallel(matchedDocuments, function (docEntry) {
            var doc         = docEntry.doc,
                oneResult   = new $.Deferred();

            // doc will be undefined if we hit the cache
            if (!doc) {
                DocumentManager.getDocumentForPath(docEntry.fileInfo.fullPath)
                    .done(function (fetchedDoc) {
                        _computeOffsets(fetchedDoc, functionName, docEntry.functions, rangeResults);
                    })
                    .always(function () {
                        oneResult.resolve();
                    });
            } else {
                _computeOffsets(doc, functionName, docEntry.functions, rangeResults);
                oneResult.resolve();
            }

            return oneResult.promise();
        }).done(function () {
            result.resolve(rangeResults);
        });

        return result.promise();
    }

    /**
     * Resolves with a record containing the Document or FileInfo and an Array of all
     * function names with offsets for the specified file. Results may be cached.
     * @param {FileInfo} fileInfo
     * @return {$.Promise} A promise resolved with a document info object that
     *   contains a map of all function names from the document and each function's start offset.
     */
    function _getFunctionsForFile(fileInfo) {
        var result = new $.Deferred();

        _shouldGetFromCache(fileInfo)
            .done(function (useCache) {
                if (useCache) {
                    // Return cached data. doc property is undefined since we hit the cache.
                    // _getOffsets() will fetch the Document if necessary.
                    result.resolve({/*doc: undefined,*/fileInfo: fileInfo, functions: fileInfo.JSUtils.functions});
                } else {
                    _readFile(fileInfo, result);
                }
            }).fail(function (err) {
                result.reject(err);
            });

        return result.promise();
    }

    /**
     * @private
     * Get all functions for each FileInfo.
     * @param {Array.<FileInfo>} fileInfos
     * @return {$.Promise} A promise resolved with an array of document info objects that each
     *   contain a map of all function names from the document and each function's start offset.
     */
    function _getFunctionsInFiles(fileInfos) {
        var result          = new $.Deferred(),
            docEntries      = [];

        PerfUtils.markStart(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS);

        Async.doInParallel(fileInfos, function (fileInfo) {
            var oneResult = new $.Deferred();

            _getFunctionsForFile(fileInfo)
                .done(function (docInfo) {
                    docEntries.push(docInfo);
                })
                .always(function (error) {
                    // If one file fails, continue to search
                    oneResult.resolve();
                });

            return oneResult.promise();
        }).always(function () {
            // Reset ChangedDocumentTracker now that the cache is up to date.
            _changedDocumentTracker.reset();

            PerfUtils.addMeasurement(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS);
            result.resolve(docEntries);
        });

        return result.promise();
    }

    /**
     * Return all functions that have the specified name.
     *
     * @param {!String} functionName The name to match.
     * @param {!Array.<FileIndexManager.FileInfo>} fileInfos The array of files to search.
     * @return {$.Promise} that will be resolved with an Array of objects containing the
     *      source document, start line, and end line (0-based, inclusive range) for each matching function list.
     *      Does not addRef() the documents returned in the array.
     */
    function findMatchingFunctions(functionName, fileInfos) {
        var result          = new $.Deferred(),
            jsFiles         = [],
            docEntries      = [];

        // Filter fileInfos for .js files
        jsFiles = fileInfos.filter(function (fileInfo) {
            return (/^\.js/i).test(PathUtils.filenameExtension(fileInfo.fullPath));
        });

        // RegExp search (or cache lookup) for all functions in the project
        _getFunctionsInFiles(jsFiles).done(function (docEntries) {
            // Compute offsets for all matched functions
            _getOffsetsForFunction(docEntries, functionName).done(function (rangeResults) {
                result.resolve(rangeResults);
            });
        });

        return result.promise();
    }

    /**
     * Finds all instances of the specified functionName in "text".
     * Returns an Array of Objects with start and end properties.
     *
     * @param text {!String} JS text to search
     * @param functionName {!String} function name to search for
     * @return {Array.<{offset:number, functionName:string}>}
     *      Array of objects containing the start offset for each matched function name.
     */
    function findAllMatchingFunctionsInText(text, functionName) {
        var allFunctions = _findAllFunctionsInText(text);
        var result = [];
        var lines = text.split("\n");
        var key;

        function addFunctionEntry(funcEntry) {
            var endOffset = _getFunctionEndOffset(text, funcEntry.offsetStart);
            result.push({
                name: key,
                lineStart: StringUtils.offsetToLineNum(lines, funcEntry.offsetStart),
                lineEnd: StringUtils.offsetToLineNum(lines, endOffset)
            });
        }

        for (key in allFunctions) {
            if (allFunctions.hasOwnProperty(key)) {
                if (key === functionName || functionName === "*") {
                    allFunctions[key].forEach(addFunctionEntry);
                }
            }
        }

        return result;
    }

    PerfUtils.createPerfMeasurement("JSUTILS_GET_ALL_FUNCTIONS", "Parallel file search across project");
    PerfUtils.createPerfMeasurement("JSUTILS_REGEXP", "RegExp search for all functions");
    PerfUtils.createPerfMeasurement("JSUTILS_END_OFFSET", "Find end offset for a single matched function");

    exports.findAllMatchingFunctionsInText = findAllMatchingFunctionsInText;
    exports._getFunctionEndOffset = _getFunctionEndOffset; // For testing only
    exports.findMatchingFunctions = findMatchingFunctions;
});
<|endoftext|>


<commit_before>"use strict";

function last(array) {
	return array[array.length - 1];
}

var dx = [-1, 0, 1, 0], dy = [0, -1, 0, 1];

var NodeTypes = {
	ROAD: 0,
	DOT: 1
};

class Node {

	constructor(type, color) {
		this.type = type;
		this.color = color;
	}

}

class WayNode extends Node {

	constructor(type, color, x, y) {
		super(type, color);
		this.x = x;
		this.y = y;
	}
}

class Level {

	constructor(field) {
		this.field = new Array(field.length);
		for (let i = 0; i < field.length; ++i) {
			let row = new Array(field[i].length);
			for (let j = 0; j < field[i].length; ++j)
				row[j] = new Node(field[i][j] ? NodeTypes.DOT : NodeTypes.ROAD, field[i][j]);
			this.field[i] = row;
		}
		this.ways = [];
	}

	nodeColor(x, y) {
		return this.field[y][x].color;
	}

	placeColor(x, y, color) {
		this.field[y][x].color = color;
	}

	canStartEnd(x, y) {
		return this.field[y][x].type == NodeTypes.DOT;
	}

	dotsForColor(color) {
		let result = [];
		for (let i = 0; i < this.field.length; ++i)
			for (let j = 0; j < this.field[i].length; ++j)
				if (this.field[i][j].color == color && this.field[i][j].type == NodeTypes.DOT)
					result.push({x: j, y: i});
		return result.length ? result : null;
	}

	wayIndex(color) {
		for (let i = 0; i < this.ways.length; ++i)
			if (this.ways[i][0].color == color)
				return i;
		return -1;
	}

	clearWay(color) {
		let index = this.wayIndex(color);
		if (index < 0)
			return;
		let way = this.ways[index];
		if (!way)
			return;
		for (let i = 0; i < way.length; ++i)
			if (way[i].type == NodeTypes.ROAD)
				this.field[way[i].y][way[i].x].color = 0;
		this.ways.splice(index, 1);
	}

	checkWay(color) {
		let way = this.ways[this.wayIndex(color)];
		return way && last(way).type == NodeTypes.DOT;
	}

	addWay(way) {
		this.ways.push(way);
	}

	checkAnswer() {
		for (let i = 0; i < this.field.length; ++i)
			for (let j = 0; j < this.field[i].length; ++j)
				if (!this.field[i][j].color || (this.canStartEnd(j, i) && !this.checkWay(this.field[i][j].color)))
					return false;
		return true;
	}

}

var presetLevels = [
		[[1, 0, 0],
		[0, 0, 0],
		[0, 0, 1]]
];

class Game {

	constructor(view) {
		this.view = view;
		this.isMouseDown = false;
		this.isGameFinished = false;
		this.generator = new Generator();
		this.levelWidth = this.levelHeight = 5;
	}

	changeLevelDimensions(width, height) {
		this.levelWidth = width;
		this.levelHeight = height;
	}

	selectLevel(levelNumber) {
		this.levelNumber = levelNumber;
	}

	startNewGame() {
		this.isGameFinished = false;
		this.level = new Level(this.levelNumber < 0 ?
			this.generator.generate(this.levelWidth, this.levelHeight) :
			presetLevels[this.levelNumber]);
		//Invalidate view
	}

	startWay(x, y) {
		let color = this.level.nodeColor(x, y);
		let dots = this.level.dotsForColor(color);
		if (!dots)
			return false;
		this.level.clearWay(color);
		this.way.push(new WayNode(NodeTypes.DOT, this.level.nodeColor(x, y), x, y));
		return true;
	}

	canContinue(x, y) {
		for (let i = 0; i < 4; ++i) {
			let nx = x + dx[i], ny = y = dy[i];
			if (nx == last(this.way).x && ny == last(this.way).y)
				return true;
		}
		return false;
	}

	continueWay(x, y) {
		let color = this.level.nodeColor(x, y);
		if (color && color != last(this.field).color)
			return false;
		if (!color) {
			if (!this.canContinue(x, y))
				return false;
			this.level.placeColor(x, y, color);
			this.way.push(new WayNode(NodeTypes.ROAD, color, x, y));
			return true;
		}
		if (this.way.length == 1)
			return false;
		if (color == this.way[0].color) {
			while (this.way.length > 1 && last(this.way).x != x && last(this.way).y != y ) {
				this.level.placeColor(last(this.way).x, last(this.way).y, 0);
				this.way.pop();
			}
			return true;
		}
		return false;
	}

	endWay(x, y) {
		if (last(this.way).x != x && last(this.way).y != y && this.canContinue(x, y))
			this.way.push(new WayNode(NodeTypes.DOT, this.way[0].color, x, y));
		this.level.addWay(this.way);
		this.way = [];
	}

	fieldMouseDown(x, y) {
		if (!this.isGameFinished && this.level.canStartEnd(x, y) && this.startWay(x, y)) {
			this.isMouseDown = true;
			//Invalidate view
		}
	}

	fieldMouseMove(x, y) {
		if (this.isMouseDown && this.continueWay(x, y)) {
			//Invalidate view
		}
	}

	fieldMouseUp(x, y) {
		if (this.level.canStartEnd(x, y) && this.level.checkAnswer()) {
			this.isGameFinished = true;
			//Invalidate view
		}
		this.endWay(x, y);
		this.isMouseDown = false;
	}
}<commit_msg>Fix game logic<commit_after>"use strict";

function last(array) {
	return array[array.length - 1];
}

//var dx = [-1, 0, 1, 0], dy = [0, -1, 0, 1];

var NodeTypes = {
	ROAD: 0,
	DOT: 1
};

class Node {

	constructor(type, color) {
		this.type = type;
		this.color = color;
	}

}

class WayNode extends Node {

	constructor(type, color, x, y) {
		super(type, color);
		this.x = x;
		this.y = y;
	}
}

class Level {

	constructor(field) {
		this.field = new Array(field.length);
		for (let i = 0; i < field.length; ++i) {
			let row = new Array(field[i].length);
			for (let j = 0; j < field[i].length; ++j)
				row[j] = new Node(field[i][j] ? NodeTypes.DOT : NodeTypes.ROAD, field[i][j]);
			this.field[i] = row;
		}
		this.ways = [];
	}

	nodeColor(x, y) {
		return this.field[y][x].color;
	}

	placeColor(x, y, color) {
		this.field[y][x].color = color;
	}

	canStartEnd(x, y) {
		return this.field[y][x].type == NodeTypes.DOT;
	}

	dotsForColor(color) {
		let result = [];
		for (let i = 0; i < this.field.length; ++i)
			for (let j = 0; j < this.field[i].length; ++j)
				if (this.field[i][j].color == color && this.field[i][j].type == NodeTypes.DOT)
					result.push({x: j, y: i});
		return result.length ? result : null;
	}

	wayIndex(color) {
		for (let i = 0; i < this.ways.length; ++i)
			if (this.ways[i][0].color == color)
				return i;
		return -1;
	}

	clearWay(color) {
		let index = this.wayIndex(color);
		if (index < 0)
			return;
		let way = this.ways[index];
		if (!way)
			return;
		for (let i = 0; i < way.length; ++i)
			if (way[i].type == NodeTypes.ROAD)
				this.field[way[i].y][way[i].x].color = 0;
		this.ways.splice(index, 1);
	}

	checkWay(color) {
		let way = this.ways[this.wayIndex(color)];
		return way && last(way).type == NodeTypes.DOT;
	}

	addWay(way) {
		this.ways.push(way);
	}

	checkAnswer() {
		for (let i = 0; i < this.field.length; ++i)
			for (let j = 0; j < this.field[i].length; ++j)
				if (!this.field[i][j].color || (this.canStartEnd(j, i) && !this.checkWay(this.field[i][j].color)))
					return false;
		return true;
	}

}

var presetLevels = [
	[[1, 0, 0],
		[0, 0, 0],
		[0, 0, 1]],

	[[1, 0],
		[0, 1]]
];

class Game {

	constructor(view) {
		this.view = view;
		this.isMouseDown = false;
		this.isGameFinished = false;
		this.generator = new Generator();
		this.levelWidth = this.levelHeight = 5;
		this.levelNumber = 0;
		this.way = [];
	}

	changeLevelDimensions(width, height) {
		this.levelWidth = width;
		this.levelHeight = height;
	}

	selectLevel(levelNumber) {
		this.levelNumber = levelNumber;
	}

	startNewGame() {
		this.isGameFinished = false;
		this.way = [];
		this.level = new Level(this.levelNumber < 0 ?
			this.generator.generate(this.levelWidth, this.levelHeight).puzzle :
			presetLevels[this.levelNumber]);
		//Inv
	}

	startWay(x, y) {
		let color = this.level.nodeColor(x, y);
		this.level.clearWay(color);
		this.way.push(new WayNode(NodeTypes.DOT, this.level.nodeColor(x, y), x, y));
		return true;
	}

	canContinue(x, y) {
		for (let i = 0; i < 4; ++i) {
			let nx = x + dx[i], ny = y + dy[i];
			if (nx == last(this.way).x && ny == last(this.way).y)
				return true;
		}
		return false;
	}

	continueWay(x, y) {
		let color = this.level.nodeColor(x, y);
		if (color && color != last(this.way).color)
			return false;
		if (!color) {
			if (!this.canContinue(x, y))
				return false;
			this.level.placeColor(x, y, this.way[0].color);
			this.way.push(new WayNode(NodeTypes.ROAD, this.way[0].color, x, y));
			return true;
		}
		if (this.way.length == 1)
			return false;
		if (color == this.way[0].color) {
			if (this.level.canStartEnd(x, y))
				return true;
			while (this.way.length > 1 && (last(this.way).x != x || last(this.way).y != y)) {
				this.level.placeColor(last(this.way).x, last(this.way).y, 0);
				this.way.pop();
			}
			return true;
		}
		return false;
	}

	endWay(x, y) {
		if ((last(this.way).x != x && last(this.way).y != y || this.level.canStartEnd(x, y)) && this.canContinue(x, y))
			this.way.push(new WayNode(NodeTypes.DOT, this.way[0].color, x, y));
		this.level.addWay(this.way);
		this.way = [];
	}

	fieldMouseDown(x, y) {
		if (!this.isGameFinished && this.level.canStartEnd(x, y) && this.startWay(x, y)) {
			this.isMouseDown = true;
			//Inv
		}
	}

	fieldMouseMove(x, y) {
		if (this.isMouseDown && this.continueWay(x, y)) {
			//Inv
		}
	}

	fieldMouseUp(x, y) {
		this.endWay(x, y);
		if (this.level.canStartEnd(x, y) && this.level.checkAnswer()) {
			this.isGameFinished = true;
			//Inv
		}
		this.isMouseDown = false;
	}
}<|endoftext|>


<commit_before>L.UGeoJSONLayer = L.GeoJSON.extend({
    options: {
      debug: false,
      light: true,
      usebbox: false,
      endpoint: "-1",
      parameters: {},
      headers: {},
      maxRequests: 5,
      pollTime:0,
      once: false,
      enctype: "form-data", //urlencoded || form-data || json
      afterFetch: function() {},
      after: function(data){}
    },

    callback: function(data) {
      if(this.options.light)
      {
        this.clearLayers();//if needed, we clean the layers
      }

      //Then we add the new data
      this.addData(data);
      this.options.after(data);
    },

  initialize: function (uOptions, options) {
    L.GeoJSON.prototype.initialize.call(this, undefined, options);
    L.Util.setOptions(this, uOptions);

    this._layersOld = [];
    this._requests = [];
  },

  onMoveEnd: function () {
    if (this.options.debug) {
      console.debug("load Data");
    }

    while(this._requests.length > this.options.maxRequests) //This allows to stop the oldest requests
    {
      this._requests.shift().abort();
    }

    var postData = {};

    for(var k in this.options.parameters)
    {
      if(this.options.parameters[k].scope != undefined)
      {
        postData[k]=this.options.parameters[k].scope[k];
      }
      else
      {
          if(typeof(postData[k]=this.options.parameters[k])=="function")
            postData[k]=this.options.parameters[k]();
          else {
            postData[k]=this.options.parameters[k];
          }
      }
    }

    var bounds = this._map.getBounds();

    if ( this.options.usebbox ) {
      postData.bbox = bounds.toBBoxString();
    } else {
      postData.south = bounds.getSouth();
      postData.north = bounds.getNorth();
      postData.east = bounds.getEast();
      postData.west = bounds.getWest();
    }
    postData.zoom = this._map.getZoom();

    var self = this;
    var request = new XMLHttpRequest();
    request.open("POST", this.options.endpoint, true);
    for(var k in this.options.headers)
    {
      if(this.options.headers[k].scope != undefined)
      {
        request.setRequestHeader(k,this.options.headers[k].scope[k]);
      }
      else
      {
        request.setRequestHeader(k,this.options.headers[k]);
      }
    }

    request.onload = function() {
      for(var i in self._requests)
      {
        if(self._requests[i] === request)
        {
          self._requests.splice(i,1); //We remove the request from the list of currently running requests.
          break;
        }
      }

      if (this.status >= 200 && this.status < 400) {
        self.options.afterFetch();
        self.callback(JSON.parse(this.responseText));
      }
    };

    this._requests.push(request);

  if(this.options.enctype=="urlencoded" || this.options.enctype=="json"){
    if(this.options.enctype=="urlencoded") {
      // urlencoded request
      var urlencoded="";
      for(var p in postData){
        if(urlencoded.length>0) urlencoded+="&";
        urlencoded+=encodeURIComponent(p)+"="+encodeURIComponent(postData[p]);
      }
      request.send(urlencoded);
    } else{
      // json request
      request.send(JSON.stringify(postData));
    }

  } else {
    var postFormData = new FormData();
    for (var p in postData){
      postFormData.append(p, postData[p]);
    }
    request.send(postFormData);
  }

  },

  onAdd: function (map) {
    this._map = map;

    if (this.options.endpoint != undefined && this.options.endpoint != "-1") {
		  this.onMoveEnd();

      if(!this.options.once) {
        map.on('dragend', this.onMoveEnd, this);
        map.on('zoomend', this.onMoveEnd, this);
        map.on("refresh", this.onMoveEnd, this);

        if (this.options.pollTime > 0) {
          this.intervalID = window.setInterval(this.onMoveEnd.bind(this), this.options.pollTime);
        }
      }
    }

    if (this.options.debug) {
      console.debug("add layer");
    }
  },

  onRemove: function (map) {
    if (this.options.debug) {
      console.debug("remove layer");
    }
    L.LayerGroup.prototype.onRemove.call(this, map);

    if (!this.options.once && this.options.pollTime > 0) {
      window.clearInterval(this.intervalID);
    }

    while(this._requests.length > 0)
    {
      this._requests.shift().abort();
    }

    if(!this.options.once) {
      map.off({
        'dragend': this.onMoveEnd
      }, this);
      map.off({
        'zoomend': this.onMoveEnd
      }, this);
        map.off({
        'refresh': this.onMoveEnd
      }, this);
    }

    this._map = null;
  }

});

L.uGeoJSONLayer = function (uOptions, options) {
  return new L.UGeoJSONLayer(uOptions, options);
};
<commit_msg>Fix parameters option handling<commit_after>L.UGeoJSONLayer = L.GeoJSON.extend({
    options: {
      debug: false,
      light: true,
      usebbox: false,
      endpoint: "-1",
      parameters: {},
      headers: {},
      maxRequests: 5,
      pollTime:0,
      once: false,
      enctype: "form-data", //urlencoded || form-data || json
      afterFetch: function() {},
      after: function(data){}
    },

    callback: function(data) {
      if(this.options.light)
      {
        this.clearLayers();//if needed, we clean the layers
      }

      //Then we add the new data
      this.addData(data);
      this.options.after(data);
    },

  initialize: function (uOptions, options) {
    L.GeoJSON.prototype.initialize.call(this, undefined, options);
    L.Util.setOptions(this, uOptions);

    this._layersOld = [];
    this._requests = [];
  },

  onMoveEnd: function () {
    if (this.options.debug) {
      console.debug("load Data");
    }

    while(this._requests.length > this.options.maxRequests) //This allows to stop the oldest requests
    {
      this._requests.shift().abort();
    }

    var postData = {};
    if (typeof this.options.parameters === 'function') {
      postData = this.options.parameters();
    } else {
      for(var k in this.options.parameters)
      {
        if(this.options.parameters[k].scope != undefined)
        {
	  postData[k]=this.options.parameters[k].scope[k];
        }
        else
        {
	  postData[k]=this.options.parameters[k];
        }
      }
    }

    var bounds = this._map.getBounds();

    if ( this.options.usebbox ) {
      postData.bbox = bounds.toBBoxString();
    } else {
      postData.south = bounds.getSouth();
      postData.north = bounds.getNorth();
      postData.east = bounds.getEast();
      postData.west = bounds.getWest();
    }
    postData.zoom = this._map.getZoom();

    var self = this;
    var request = new XMLHttpRequest();
    request.open("POST", this.options.endpoint, true);
    for(var k in this.options.headers)
    {
      if(this.options.headers[k].scope != undefined)
      {
        request.setRequestHeader(k,this.options.headers[k].scope[k]);
      }
      else
      {
        request.setRequestHeader(k,this.options.headers[k]);
      }
    }

    request.onload = function() {
      for(var i in self._requests)
      {
        if(self._requests[i] === request)
        {
          self._requests.splice(i,1); //We remove the request from the list of currently running requests.
          break;
        }
      }

      if (this.status >= 200 && this.status < 400) {
        self.options.afterFetch();
        self.callback(JSON.parse(this.responseText));
      }
    };

    this._requests.push(request);

  if(this.options.enctype=="urlencoded" || this.options.enctype=="json"){
    if(this.options.enctype=="urlencoded") {
      // urlencoded request
      var urlencoded="";
      for(var p in postData){
        if(urlencoded.length>0) urlencoded+="&";
        urlencoded+=encodeURIComponent(p)+"="+encodeURIComponent(postData[p]);
      }
      request.send(urlencoded);
    } else{
      // json request
      request.send(JSON.stringify(postData));
    }

  } else {
    var postFormData = new FormData();
    for (var p in postData){
      postFormData.append(p, postData[p]);
    }
    request.send(postFormData);
  }

  },

  onAdd: function (map) {
    this._map = map;

    if (this.options.endpoint != undefined && this.options.endpoint != "-1") {
		  this.onMoveEnd();

      if(!this.options.once) {
        map.on('dragend', this.onMoveEnd, this);
        map.on('zoomend', this.onMoveEnd, this);
        map.on("refresh", this.onMoveEnd, this);

        if (this.options.pollTime > 0) {
          this.intervalID = window.setInterval(this.onMoveEnd.bind(this), this.options.pollTime);
        }
      }
    }

    if (this.options.debug) {
      console.debug("add layer");
    }
  },

  onRemove: function (map) {
    if (this.options.debug) {
      console.debug("remove layer");
    }
    L.LayerGroup.prototype.onRemove.call(this, map);

    if (!this.options.once && this.options.pollTime > 0) {
      window.clearInterval(this.intervalID);
    }

    while(this._requests.length > 0)
    {
      this._requests.shift().abort();
    }

    if(!this.options.once) {
      map.off({
        'dragend': this.onMoveEnd
      }, this);
      map.off({
        'zoomend': this.onMoveEnd
      }, this);
        map.off({
        'refresh': this.onMoveEnd
      }, this);
    }

    this._map = null;
  }

});

L.uGeoJSONLayer = function (uOptions, options) {
  return new L.UGeoJSONLayer(uOptions, options);
};
<|endoftext|>


<commit_before>/* global cozy */
import * as accounts from './accounts'
import * as konnectors from './konnectors'
import * as jobs from './jobs'
import { randomDayTime } from './daytime'

import { createKonnectorTrigger } from '../ducks/triggers'
import { launchTriggerAndQueue } from '../ducks/connections'
import { createAccount } from '../ducks/accounts'

const KONNECTORS_DOCTYPE = 'io.cozy.konnectors'

export const CONNECTION_STATUS = {
  ERRORED: 'errored',
  RUNNING: 'running',
  CONNECTED: 'connected'
}

const INSTALL_TIMEOUT = 120 * 1000

export default class CollectStore {
  constructor(connectors, context, options = {}) {
    this.listener = null
    this.options = options

    this.categories = require('../config/categories')
    this.driveUrl = null

    this.initializeRealtime()
  }

  initializeRealtime() {
    jobs
      .subscribeAll(cozy.client)
      .then(subscription => {
        subscription
          .onCreate(job => this.updateUnfinishedJob(job))
          .onUpdate(job => this.updateUnfinishedJob(job))
        // .onDelete(job => this.deleteRunningJob(job))
      })
      .catch(error => {
        console.warn &&
          console.warn(`Cannot initialize realtime for jobs: ${error.message}`)
      })

    konnectors.subscribeAll(cozy.client).then(subscription => {
      subscription.onCreate(konnector =>
        this.handleInstalledKonnector(konnector)
      )
    })
  }

  updateUnfinishedJob(job) {
    const normalized = {
      ...job,
      ...job.attributes,
      id: job._id,
      _type: 'io.cozy.jobs'
    }

    this.dispatch({
      type: 'RECEIVE_NEW_DOCUMENT',
      response: { data: [normalized] },
      updateCollections: ['jobs']
    })
  }

  handleInstalledKonnector(konnector) {
    const normalized = {
      ...konnector,
      ...konnector.attributes,
      id: `${KONNECTORS_DOCTYPE}/${konnector.slug}`,
      _type: KONNECTORS_DOCTYPE
    }

    this.dispatch({
      type: 'RECEIVE_NEW_DOCUMENT',
      response: { data: [normalized] },
      updateCollections: ['konnectors']
    })
  }

  // Get the drive application url using the list of application
  // FIXME this works only because we need the application list permission for the cozy-bar
  // and this also supposes that the drive application has the 'drive' slug
  fetchDriveUrl() {
    return cozy.client
      .fetchJSON('GET', '/apps/')
      .then(body => {
        const driveapp = body.find(item => item.attributes.slug === 'drive')
        if (driveapp && driveapp.links) {
          this.driveUrl = `${driveapp.links.related}#/files/`
        }
      })
      .catch(err => {
        console.warn(err)
        return false
      })
  }

  // FIXME: should be handled in a cozy-drive inter-app
  fetchFolders() {
    return cozy.client.data
      .findAll('io.cozy.files')
      .then(result => {
        // if path contains '/.', it contains an hidden folder
        const folders = result
          .filter(f => f.type === 'directory' && !f.path.match(/(?:\/\.)/))
          .sort((a, b) => a.path > b.path)
        return folders
      })
      .catch(err => {
        console.warn(err)
        return []
      })
  }

  createDirectoryIfNecessary(folderPath) {
    if (folderPath) {
      return cozy.client.files.createDirectoryByPath(folderPath)
    } else {
      return Promise.resolve(null)
    }
  }

  // Account connection workflow, see
  // https://github.com/cozy/cozy-stack/blob/master/docs/konnectors_workflow_example.md
  connectAccount(konnector, account, disableEnqueue, enqueueAfter = 10000) {
    const startTime = new Date().getTime()

    // return object to store all business object implied in the connection
    const connection = {}
    // detect oauth case
    const isOAuth = !!account.oauth

    // 1. Create folder, will be replaced by an intent or something else
    return (
      this.createDirectoryIfNecessary(!!account.auth && account.auth.folderPath)
        // 2. Create account
        .then(folder => {
          connection.folder = folder
          const folderId = folder ? folder._id : null
          if (isOAuth) {
            const newAttributes = {}

            if (folderId) {
              newAttributes.folderId = folderId
            }

            return accounts.update(
              cozy.client,
              account,
              Object.assign({}, account, newAttributes)
            )
          } else {
            return this.dispatch(
              createAccount({
                auth: account.auth,
                account_type: konnector.slug
              })
            ).then(result => {
              // Temporary hack ot return account
              return result.data[0]
            })
          }
        })
        // 3. Konnector installation
        .then(account => {
          connection.account = account

          return new Promise((resolve, reject) => {
            let enqueued = false
            let enqueueTimeout

            const enqueue = () => {
              clearTimeout(enqueueTimeout)
              enqueued = true
              resolve(connection)
            }

            konnectors
              .install(cozy.client, konnector, INSTALL_TIMEOUT)
              // 4. Add account to konnector
              .then(installedkonnector => {
                return konnectors.addAccount(
                  cozy.client,
                  installedkonnector,
                  connection.account
                )
              })
              // 5. Add permissions to folder for konnector if folder created
              .then(completeKonnector => {
                connection.konnector = { ...konnector, ...completeKonnector }
                if (!connection.folder) return Promise.resolve()
                return konnectors.addFolderPermission(
                  cozy.client,
                  completeKonnector,
                  connection.folder._id
                )
              })
              // 6. Reference konnector in folder
              .then(permission => {
                if (!permission) return Promise.resolve()
                connection.permission = permission
                return cozy.client.data.addReferencedFiles(
                  connection.konnector,
                  connection.folder._id
                )
              })
              // 7. Create trigger
              .then(() =>
                this.dispatch(
                  createKonnectorTrigger(
                    connection.konnector,
                    connection.account,
                    connection.folder,
                    {
                      // We always pass a default day value,
                      // `createKonnectorTrigger` will
                      // then use it if it needs.
                      day: new Date().getDay(),
                      ...randomDayTime(
                        konnector.timeInterval ||
                          this.options.defaultTriggerTimeInterval
                      )
                    }
                  )
                )
              )
              .then(result => result.data[0])
              // 8. Run a job for the konnector
              .then(trigger =>
                this.dispatch(
                  launchTriggerAndQueue(trigger, konnector.loginDelay)
                )
              )
              .then(result => result.data[0])
              // 9. Handle job
              .then(job => {
                connection.job = job

                const state = job.state || job.attributes.state
                connection.successTimeout = ![
                  konnectors.JOB_STATE.ERRORED,
                  konnectors.JOB_STATE.DONE
                ].includes(state)
              })
              .then(() => {
                enqueue()
              })
              .catch(error => {
                connection.error = error
              })
              .then(() => {
                clearTimeout(enqueueTimeout)
                if (!enqueued) {
                  resolve(connection)
                }
              })

            if (!disableEnqueue) {
              const elapsedTime = startTime - new Date().getTime()

              if (elapsedTime >= enqueueAfter) {
                enqueue()
              } else {
                enqueueTimeout = setTimeout(enqueue, enqueueAfter - elapsedTime)
              }
            }
          })
        })
    )
  }

  /**
   * runAccount Runs an account
   * @param {object}  connector            A connector
   * @param {object}  account              the account to run, must belong to the connector
   * @param {Boolean} disableEnqueue Boolean to disable a success timeout in the run method. Used by example by the onboarding
   * @returns The run result or a resulting error
   */
  runAccount(
    trigger,
    connector,
    account,
    disableEnqueue,
    enqueueAfter = 10000
  ) {
    // TODO: mutualize this part with connectAccount
    return this.dispatch(launchTriggerAndQueue(trigger))
  }

  /**
   * updateAccount : updates an account in a connector in DB with new values
   * @param {Object} connector The connector to update
   * @param {Object} account   The account to update
   * @param {Object} values    The new values of the updated account
   * @returns {Object} The up to date connector
   */
  updateAccount(connector, account, values) {
    // Save the previous state
    const previousAccount = Object.assign({}, account)
    const newAccount = Object.assign({}, account)
    // merge values in account
    newAccount.auth = Object.assign(newAccount.auth, values)

    return accounts
      .update(cozy.client, previousAccount, newAccount)
      .catch(error => {
        return Promise.reject(error)
      })
  }

  updateFolderPath(connector, account, folderId, values, t) {
    // Update file
    return cozy.client.files
      .updateAttributesById(folderId, {
        name: values.namePath,
        path: `${values.folderPath}/${values.namePath}`,
        dir_id: values.dir_id
      })
      .then(() => {
        // Update Account
        this.updateAccount(connector, account, values)
      })
      .catch(error => {
        return Promise.reject(error)
      })
  }

  createIntentService(intent, window) {
    return cozy.client.intents.createService(intent, window)
  }
}
<commit_msg>feat: install konnector before account and trigger creation ✨<commit_after>/* global cozy */
import * as accounts from './accounts'
import * as konnectors from './konnectors'
import * as jobs from './jobs'
import { randomDayTime } from './daytime'

import { createKonnectorTrigger } from '../ducks/triggers'
import { launchTriggerAndQueue } from '../ducks/connections'
import { createAccount } from '../ducks/accounts'

const KONNECTORS_DOCTYPE = 'io.cozy.konnectors'

export const CONNECTION_STATUS = {
  ERRORED: 'errored',
  RUNNING: 'running',
  CONNECTED: 'connected'
}

const INSTALL_TIMEOUT = 120 * 1000

export default class CollectStore {
  constructor(connectors, context, options = {}) {
    this.listener = null
    this.options = options

    this.categories = require('../config/categories')
    this.driveUrl = null

    this.initializeRealtime()
  }

  initializeRealtime() {
    jobs
      .subscribeAll(cozy.client)
      .then(subscription => {
        subscription
          .onCreate(job => this.updateUnfinishedJob(job))
          .onUpdate(job => this.updateUnfinishedJob(job))
        // .onDelete(job => this.deleteRunningJob(job))
      })
      .catch(error => {
        console.warn &&
          console.warn(`Cannot initialize realtime for jobs: ${error.message}`)
      })

    konnectors.subscribeAll(cozy.client).then(subscription => {
      subscription.onCreate(konnector =>
        this.handleInstalledKonnector(konnector)
      )
    })
  }

  updateUnfinishedJob(job) {
    const normalized = {
      ...job,
      ...job.attributes,
      id: job._id,
      _type: 'io.cozy.jobs'
    }

    this.dispatch({
      type: 'RECEIVE_NEW_DOCUMENT',
      response: { data: [normalized] },
      updateCollections: ['jobs']
    })
  }

  handleInstalledKonnector(konnector) {
    const normalized = {
      ...konnector,
      ...konnector.attributes,
      id: `${KONNECTORS_DOCTYPE}/${konnector.slug}`,
      _type: KONNECTORS_DOCTYPE
    }

    this.dispatch({
      type: 'RECEIVE_NEW_DOCUMENT',
      response: { data: [normalized] },
      updateCollections: ['konnectors']
    })
  }

  // Get the drive application url using the list of application
  // FIXME this works only because we need the application list permission for the cozy-bar
  // and this also supposes that the drive application has the 'drive' slug
  fetchDriveUrl() {
    return cozy.client
      .fetchJSON('GET', '/apps/')
      .then(body => {
        const driveapp = body.find(item => item.attributes.slug === 'drive')
        if (driveapp && driveapp.links) {
          this.driveUrl = `${driveapp.links.related}#/files/`
        }
      })
      .catch(err => {
        console.warn(err)
        return false
      })
  }

  // FIXME: should be handled in a cozy-drive inter-app
  fetchFolders() {
    return cozy.client.data
      .findAll('io.cozy.files')
      .then(result => {
        // if path contains '/.', it contains an hidden folder
        const folders = result
          .filter(f => f.type === 'directory' && !f.path.match(/(?:\/\.)/))
          .sort((a, b) => a.path > b.path)
        return folders
      })
      .catch(err => {
        console.warn(err)
        return []
      })
  }

  createDirectoryIfNecessary(folderPath) {
    if (folderPath) {
      return cozy.client.files.createDirectoryByPath(folderPath)
    } else {
      return Promise.resolve(null)
    }
  }

  // Account connection workflow, see
  // https://github.com/cozy/cozy-stack/blob/master/docs/konnectors_workflow_example.md
  connectAccount(konnector, account, disableEnqueue, enqueueAfter = 10000) {
    // return object to store all business object implied in the connection
    const connection = {}
    // detect oauth case
    const isOAuth = !!account.oauth

    // 1. Konnector installation
    return (
      konnectors
        .install(cozy.client, konnector, INSTALL_TIMEOUT)
        .then(completeKonnector => {
          connection.konnector = { ...konnector, ...completeKonnector }
        })
        // 2. Create folder, will be replaced by an intent or something else
        .then(
          this.createDirectoryIfNecessary(
            !!account.auth && account.auth.folderPath
          ).then(folder => {
            if (!folder) return Promise.resolve()
            connection.folder = folder
            // 3. Reference konnector in folder
            return konnectors
              .addFolderPermission(
                cozy.client,
                connection.konnector,
                connection.folder._id
              )
              .then(permission => {
                connection.permission = permission
                return cozy.client.data.addReferencedFiles(
                  connection.konnector,
                  connection.folder._id
                )
              })
          })
        )
        // 4. Creates account
        .then(() => {
          if (isOAuth) {
            return account
          }

          return this.dispatch(
            createAccount({
              auth: account.auth,
              account_type: konnector.slug
            })
          ).then(result => {
            // Return account
            return result.data[0]
          })
        })
        .then(account => {
          connection.account = account
        })
        // 5. Create trigger
        .then(() =>
          this.dispatch(
            createKonnectorTrigger(
              connection.konnector,
              connection.account,
              connection.folder,
              {
                // We always pass a default day value,
                // `createKonnectorTrigger` will
                // then use it if it needs.
                day: new Date().getDay(),
                ...randomDayTime(
                  konnector.timeInterval ||
                    this.options.defaultTriggerTimeInterval
                )
              }
            )
          ).then(result => result.data[0])
        )
        // 6. Launch trigger for the konnector
        .then(trigger =>
          this.dispatch(
            launchTriggerAndQueue(trigger, konnector.loginDelay)
          ).then(result => result.data[0])
        )
        // 7. Handle job
        .then(job => {
          connection.job = job
        })
        .catch(error => {
          connection.error = error
        })
        // 8. Returns connection
        .then(() => connection)
    )
  }

  /**
   * runAccount Runs an account
   * @param {object}  connector            A connector
   * @param {object}  account              the account to run, must belong to the connector
   * @param {Boolean} disableEnqueue Boolean to disable a success timeout in the run method. Used by example by the onboarding
   * @returns The run result or a resulting error
   */
  runAccount(
    trigger,
    connector,
    account,
    disableEnqueue,
    enqueueAfter = 10000
  ) {
    // TODO: mutualize this part with connectAccount
    return this.dispatch(launchTriggerAndQueue(trigger))
  }

  /**
   * updateAccount : updates an account in a connector in DB with new values
   * @param {Object} connector The connector to update
   * @param {Object} account   The account to update
   * @param {Object} values    The new values of the updated account
   * @returns {Object} The up to date connector
   */
  updateAccount(connector, account, values) {
    // Save the previous state
    const previousAccount = Object.assign({}, account)
    const newAccount = Object.assign({}, account)
    // merge values in account
    newAccount.auth = Object.assign(newAccount.auth, values)

    return accounts
      .update(cozy.client, previousAccount, newAccount)
      .catch(error => {
        return Promise.reject(error)
      })
  }

  updateFolderPath(connector, account, folderId, values, t) {
    // Update file
    return cozy.client.files
      .updateAttributesById(folderId, {
        name: values.namePath,
        path: `${values.folderPath}/${values.namePath}`,
        dir_id: values.dir_id
      })
      .then(() => {
        // Update Account
        this.updateAccount(connector, account, values)
      })
      .catch(error => {
        return Promise.reject(error)
      })
  }

  createIntentService(intent, window) {
    return cozy.client.intents.createService(intent, window)
  }
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

/**
 * The `auto-binding` element extends the template element. It provides a quick
 * and easy way to do data binding without the need to setup a model.
 * The `auto-binding` element itself serves as the model and controller for the
 * elements it contains. Both data and event handlers can be bound.
 *
 * The `auto-binding` element acts just like a template that is bound to
 * a model. It stamps its content in the dom adjacent to itself. When the
 * content is stamped, the `template-bound` event is fired.
 *
 * Example:
 *
 *     <template is="auto-binding">
 *
Say something: <input value="{{value}}">

 *
You said: {{value}}

 *       <button on-tap="{{buttonTap}}">Tap me!</button>
 *     </template>
 *
 *
 * @module Polymer
 * @status stable
*/

(function() {

  var element = document.createElement('polymer-element');
  element.setAttribute('name', 'auto-binding');
  element.setAttribute('extends', 'template');
  element.init();

  Polymer('auto-binding', {

    createdCallback: function() {
      this.syntax = this.bindingDelegate = this.makeSyntax();
      // delay stamping until polymer-ready so that auto-binding is not
      // required to load last.
      addEventListener('polymer-ready', function() {
        this.model = this;
        this.setAttribute('bind', '');
        // we don't bother with an explicit signal here, we could ust a MO
        // if necessary
        this.async(function() {
          // note: this will marshall *all* the elements in the parentNode
          // rather than just stamped ones. We'd need to use createInstance
          // to fix this or something else fancier.
          this.marshalNodeReferences(this.parentNode);
          // template stamping is asynchronous so stamping isn't complete
          // by polymer-ready; fire an event so users can use stamped elements
          this.fire('template-bound');
        });
      }.bind(this));
    },

    makeSyntax: function() {
      var events = Object.create(Polymer.api.declaration.events);
      var self = this;
      events.findController = function() { return self.model; };

      var syntax = new PolymerExpressions();
      var prepareBinding = syntax.prepareBinding;
      syntax.prepareBinding = function(pathString, name, node) {
        return events.prepareEventBinding(pathString, name, node) ||
               prepareBinding.call(syntax, pathString, name, node);
      };
      return syntax;
    }

  });

})();
<commit_msg>Allow stamping after 'polymer-ready' by using Polymer.whenPolymerReady instead.<commit_after>/*
 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

/**
 * The `auto-binding` element extends the template element. It provides a quick
 * and easy way to do data binding without the need to setup a model.
 * The `auto-binding` element itself serves as the model and controller for the
 * elements it contains. Both data and event handlers can be bound.
 *
 * The `auto-binding` element acts just like a template that is bound to
 * a model. It stamps its content in the dom adjacent to itself. When the
 * content is stamped, the `template-bound` event is fired.
 *
 * Example:
 *
 *     <template is="auto-binding">
 *
Say something: <input value="{{value}}">

 *
You said: {{value}}

 *       <button on-tap="{{buttonTap}}">Tap me!</button>
 *     </template>
 *
 *
 * @module Polymer
 * @status stable
*/

(function() {

  var element = document.createElement('polymer-element');
  element.setAttribute('name', 'auto-binding');
  element.setAttribute('extends', 'template');
  element.init();

  Polymer('auto-binding', {

    createdCallback: function() {
      this.syntax = this.bindingDelegate = this.makeSyntax();
      // delay stamping until polymer-ready so that auto-binding is not
      // required to load last.
      Polymer.whenPolymerReady(function() {
        this.model = this;
        this.setAttribute('bind', '');
        // we don't bother with an explicit signal here, we could ust a MO
        // if necessary
        this.async(function() {
          // note: this will marshall *all* the elements in the parentNode
          // rather than just stamped ones. We'd need to use createInstance
          // to fix this or something else fancier.
          this.marshalNodeReferences(this.parentNode);
          // template stamping is asynchronous so stamping isn't complete
          // by polymer-ready; fire an event so users can use stamped elements
          this.fire('template-bound');
        });
      }.bind(this));
    },

    makeSyntax: function() {
      var events = Object.create(Polymer.api.declaration.events);
      var self = this;
      events.findController = function() { return self.model; };

      var syntax = new PolymerExpressions();
      var prepareBinding = syntax.prepareBinding;
      syntax.prepareBinding = function(pathString, name, node) {
        return events.prepareEventBinding(pathString, name, node) ||
               prepareBinding.call(syntax, pathString, name, node);
      };
      return syntax;
    }

  });

})();
<|endoftext|>


<commit_before>//
// Strider Worker extension for Sauce Labs tests
//

var check = require('httpcheck')
var fs = require('fs')
var path = require('path')
var request = require('request')
var wd = require('wd')

// Port on which the application-under-test webserver should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var HTTP_PORT
//
// Path to SauceLabs Connector PID file
var PIDFILE = path.join(__dirname, "tunnel.pid")

// SauceLabs test timeout in ms
// Permits self-healing in worst-case hang
var SAUCE_TEST_TIMEOUT = 1000 * 60 * 45 // 45 minutes

var connectorProc

var cleanupRun = false

// Read & parse a JSON file
function getJson(filename, cb) {
  fs.readFile(filename, function(err, data) {
    if (err) return cb(err, null)
    try {
      var json = JSON.parse(data)
      cb(null, json)
    } catch(e) {
      cb(e, null)
    }
  })
}

// Is Sauce configured for this context?
function sauceConfigured(ctx) {
  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username

  if (!sauceAccessKey
    || !sauceUsername) {
    return false
  }

  return true
}

// This will shut down the tunnel in a nice way
function cleanup(ctx, cb) {
  if (!sauceConfigured(ctx)) {
    return cb(0)
  }
  cleanupRun = true
  var msg = "Shutting down Sauce Connector"
  console.log(msg)
  ctx.striderMessage(msg)
  if (connectorProc) connectorProc.kill("SIGINT")
  // Give Sauce Connector 5 seconds to gracefully stop before sending SIGKILL
  setTimeout(function() {
    if (connectorProc) connectorProc.kill("SIGKILL")
    fs.unlink(PIDFILE, function() {})
    msg = "Sauce Connector successfully shut down"
    console.log(msg)
    ctx.striderMessage(msg)

    return cb(0)
  }, 5000)
}

function test(ctx, cb) {

  if (!sauceConfigured(ctx)) {
    return cb()
  }

  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username
  var sauceBrowsers = ctx.jobData.repo_config.sauce_browsers

  if (sauceBrowsers === undefined || sauceBrowsers.length === 0) {
    // Default to latest chrome on Windows Vista
    sauceBrowsers = [
      {
        browserName:"chrome",
        version:"",
        platform:"Windows 2008"
      }
    ]
  }

  function log(msg) {
    ctx.striderMessage(msg)
    console.log(msg)
  }

  HTTP_PORT = ctx.browsertestPort || 8031

  log("Waiting for webserver to come up on localhost:" + HTTP_PORT)

  check({url:"http://localhost:"+HTTP_PORT+"/", log:log}, function(err) {
    if (err) {
      clearInterval(intervalId)
      return cb(1)
    }
    serverUp()
  })

  // Start the Sauce Connector. Returns childProcess object.
  function startConnector(username, apiKey, exitCb) {
    var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
    var jsh = ctx.shellWrap("exec java -Xmx64m -jar " + jarPath + " " + username + " " + apiKey)

    ctx.striderMessage("Starting Sauce Connector")
    connectorProc = ctx.forkProc(ctx.workingDir, jsh.cmd, jsh.args, exitCb)
    // Wait until connector outputs "You may start your tests"
    // before executing Sauce tests
    connectorProc.stdout.on('data', function(data) {
      if (/Connected! You may start your tests./.exec(data) !== null) {
          var resultsReceived = 0
          var buildStatus = 0
          var resultMessages = []
          var finished = false
          var tasks = []
          var sauceTimeoutSeconds = SAUCE_TEST_TIMEOUT / 1000
          sauceBrowsers.forEach(function(browser) {
            var worker = wd.remote("ondemand.saucelabs.com", 80,
              sauceUsername, sauceAccessKey)
            var browserId = browser.browserName + "-" + browser.version + "-" + browser.platform.replace(" ", "-")
            // ctx.browsertestPort and ctx.browsertestPath come from the `prepare` phase test
            // plugin - e.g. strider-qunit.
            var testUrl = "http://localhost:" +
                ctx.browsertestPort + "/" + browserId + ctx.browsertestPath
            worker.done = false
            worker.init({
              browserName: browser.browserName,
              version: browser.version,
              platform: browser.platform,
              name: ctx.jobData.repo_config.display_url,
              'idle-timeout': sauceTimeoutSeconds,
              'max-duration': sauceTimeoutSeconds
            }, function(err) {
              if (err) {
                log("Error creating Sauce worker: " + err)
                return cb(1)
              }
              worker.get(testUrl, function() {
                log("Created Sauce worker: " + browserId)
              })

            })

            setTimeout(function() {
              if (!worker.done) {
                log("ERROR: Timeout of " + SAUCE_TEST_TIMEOUT + " ms exceeded for " + browserId + " - terminating ")
                ctx.events.emit('testDone', { id: browserId, total:0, failed:1, passed:0, runtime: SAUCE_TEST_TIMEOUT })
              }
            }, SAUCE_TEST_TIMEOUT)
            ctx.events.on('testDone', function(result) {
              if (finished) return
              if (result.id === browserId && worker && !worker.done) {
                tasks.push({id:"sauce", data:{total:result.total, failed: result.failed, passed:result.passed, runtime:result.runtime, id:result.id}})
                resultMessages.push("Results for tests on " + result.id + ": " + result.total + " total " +
                  result.failed + " failed " + result.passed + " passed " + result.runtime + " ms runtime")
                if (result.failed !== 0) {
                  buildStatus = 1
                }
                log("Terminating Sauce worker: " + browserId)
                worker.quit()
                worker.done = true
                resultsReceived++
              }
              // If all the results are in, finish the build
              if (resultsReceived == sauceBrowsers.length) {
                finished = true
                resultMessages.forEach(function(msg) {
                  log(msg)
                })
                cb(buildStatus, tasks)
              }
            })
          })
      }
    })
  }
  // Server is up, start Sauce Connector
  function serverUp() {
    console.log("Starting sauce connector")
    startConnector(sauceUsername, sauceAccessKey,
      function(exitCode) {
      console.log("Sauce Connector exited with code: %d", exitCode)
      // If the connector exited before the cleanup phase has run, it failed to start
      if (!cleanupRun) {
        log("Error starting Sauce Connector - failing test")
        cleanupRun = true
        fs.unlink(PIDFILE, function() {})
        return cb(1)
      }
    })
  }
}

module.exports = function(ctx, cb) {

  ctx.addBuildHook({
    cleanup:cleanup,
    test:test
  })

  console.log("strider-sauce worker extension loaded")
  cb(null, null)
}
<commit_msg>don't need to clear interval in error path anymore.<commit_after>//
// Strider Worker extension for Sauce Labs tests
//

var check = require('httpcheck')
var fs = require('fs')
var path = require('path')
var request = require('request')
var wd = require('wd')

// Port on which the application-under-test webserver should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var HTTP_PORT
//
// Path to SauceLabs Connector PID file
var PIDFILE = path.join(__dirname, "tunnel.pid")

// SauceLabs test timeout in ms
// Permits self-healing in worst-case hang
var SAUCE_TEST_TIMEOUT = 1000 * 60 * 45 // 45 minutes

var connectorProc

var cleanupRun = false

// Read & parse a JSON file
function getJson(filename, cb) {
  fs.readFile(filename, function(err, data) {
    if (err) return cb(err, null)
    try {
      var json = JSON.parse(data)
      cb(null, json)
    } catch(e) {
      cb(e, null)
    }
  })
}

// Is Sauce configured for this context?
function sauceConfigured(ctx) {
  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username

  if (!sauceAccessKey
    || !sauceUsername) {
    return false
  }

  return true
}

// This will shut down the tunnel in a nice way
function cleanup(ctx, cb) {
  if (!sauceConfigured(ctx)) {
    return cb(0)
  }
  cleanupRun = true
  var msg = "Shutting down Sauce Connector"
  console.log(msg)
  ctx.striderMessage(msg)
  if (connectorProc) connectorProc.kill("SIGINT")
  // Give Sauce Connector 5 seconds to gracefully stop before sending SIGKILL
  setTimeout(function() {
    if (connectorProc) connectorProc.kill("SIGKILL")
    fs.unlink(PIDFILE, function() {})
    msg = "Sauce Connector successfully shut down"
    console.log(msg)
    ctx.striderMessage(msg)

    return cb(0)
  }, 5000)
}

function test(ctx, cb) {

  if (!sauceConfigured(ctx)) {
    return cb()
  }

  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username
  var sauceBrowsers = ctx.jobData.repo_config.sauce_browsers

  if (sauceBrowsers === undefined || sauceBrowsers.length === 0) {
    // Default to latest chrome on Windows Vista
    sauceBrowsers = [
      {
        browserName:"chrome",
        version:"",
        platform:"Windows 2008"
      }
    ]
  }

  function log(msg) {
    ctx.striderMessage(msg)
    console.log(msg)
  }

  HTTP_PORT = ctx.browsertestPort || 8031

  log("Waiting for webserver to come up on localhost:" + HTTP_PORT)

  check({url:"http://localhost:"+HTTP_PORT+"/", log:log}, function(err) {
    if (err) {
      return cb(1)
    }
    serverUp()
  })

  // Start the Sauce Connector. Returns childProcess object.
  function startConnector(username, apiKey, exitCb) {
    var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
    var jsh = ctx.shellWrap("exec java -Xmx64m -jar " + jarPath + " " + username + " " + apiKey)

    ctx.striderMessage("Starting Sauce Connector")
    connectorProc = ctx.forkProc(ctx.workingDir, jsh.cmd, jsh.args, exitCb)
    // Wait until connector outputs "You may start your tests"
    // before executing Sauce tests
    connectorProc.stdout.on('data', function(data) {
      if (/Connected! You may start your tests./.exec(data) !== null) {
          var resultsReceived = 0
          var buildStatus = 0
          var resultMessages = []
          var finished = false
          var tasks = []
          var sauceTimeoutSeconds = SAUCE_TEST_TIMEOUT / 1000
          sauceBrowsers.forEach(function(browser) {
            var worker = wd.remote("ondemand.saucelabs.com", 80,
              sauceUsername, sauceAccessKey)
            var browserId = browser.browserName + "-" + browser.version + "-" + browser.platform.replace(" ", "-")
            // ctx.browsertestPort and ctx.browsertestPath come from the `prepare` phase test
            // plugin - e.g. strider-qunit.
            var testUrl = "http://localhost:" +
                ctx.browsertestPort + "/" + browserId + ctx.browsertestPath
            worker.done = false
            worker.init({
              browserName: browser.browserName,
              version: browser.version,
              platform: browser.platform,
              name: ctx.jobData.repo_config.display_url,
              'idle-timeout': sauceTimeoutSeconds,
              'max-duration': sauceTimeoutSeconds
            }, function(err) {
              if (err) {
                log("Error creating Sauce worker: " + err)
                return cb(1)
              }
              worker.get(testUrl, function() {
                log("Created Sauce worker: " + browserId)
              })

            })

            setTimeout(function() {
              if (!worker.done) {
                log("ERROR: Timeout of " + SAUCE_TEST_TIMEOUT + " ms exceeded for " + browserId + " - terminating ")
                ctx.events.emit('testDone', { id: browserId, total:0, failed:1, passed:0, runtime: SAUCE_TEST_TIMEOUT })
              }
            }, SAUCE_TEST_TIMEOUT)
            ctx.events.on('testDone', function(result) {
              if (finished) return
              if (result.id === browserId && worker && !worker.done) {
                tasks.push({id:"sauce", data:{total:result.total, failed: result.failed, passed:result.passed, runtime:result.runtime, id:result.id}})
                resultMessages.push("Results for tests on " + result.id + ": " + result.total + " total " +
                  result.failed + " failed " + result.passed + " passed " + result.runtime + " ms runtime")
                if (result.failed !== 0) {
                  buildStatus = 1
                }
                log("Terminating Sauce worker: " + browserId)
                worker.quit()
                worker.done = true
                resultsReceived++
              }
              // If all the results are in, finish the build
              if (resultsReceived == sauceBrowsers.length) {
                finished = true
                resultMessages.forEach(function(msg) {
                  log(msg)
                })
                cb(buildStatus, tasks)
              }
            })
          })
      }
    })
  }
  // Server is up, start Sauce Connector
  function serverUp() {
    console.log("Starting sauce connector")
    startConnector(sauceUsername, sauceAccessKey,
      function(exitCode) {
      console.log("Sauce Connector exited with code: %d", exitCode)
      // If the connector exited before the cleanup phase has run, it failed to start
      if (!cleanupRun) {
        log("Error starting Sauce Connector - failing test")
        cleanupRun = true
        fs.unlink(PIDFILE, function() {})
        return cb(1)
      }
    })
  }
}

module.exports = function(ctx, cb) {

  ctx.addBuildHook({
    cleanup:cleanup,
    test:test
  })

  console.log("strider-sauce worker extension loaded")
  cb(null, null)
}
<|endoftext|>


<commit_before>import * as _collections from './collections'

const URL_REGEX = /(?:http(?:s?):\/\/)?(?:www)?\w+\.(?:[a-z]{2,})[\w\?\/\=\.\#]*/g

export const typeSafeEq = (x, y) => x.toString() === y.toString()
export const orStr = e => e || ''
export const orArr = e => e || []

const escSpecialChars = str => str.replace(/\$|\{|\}/g, e => `\\${e}`)
const toSource = re => re instanceof RegExp ? re.source : escSpecialChars(re)
const parenthesize = str => `(?:${str})`
export function or(res) {
  const strParts = res.map(toSource).filter(_collections.isPresent).map(parenthesize)
  const lengthSorted = _.sortBy(strParts, prt => -prt.length) // To avoid partial replacements.
  return new RegExp(parenthesize(lengthSorted.join('|')), 'g')
}

export function replaceByMap(str, replacementMap) {
  const regex = or(Object.keys(replacementMap))
  return str.replace(regex, match => replacementMap[match])
}

export function makeURLsMarkdown(text) {
  const fullRegex = new RegExp(`\\[.+\\]\\(${URL_REGEX.source}\\)`, 'g')
  let transformedText = ''
  let prevIndex = 0
  let matchArr
  while ((matchArr = fullRegex.exec(text)) !== null) {
    const partial = text.slice(prevIndex, matchArr.index)
    transformedText += partial.replace(URL_REGEX, match => `[${match}](${match})`)
    transformedText += matchArr[0]
    prevIndex = matchArr.index + matchArr[0].length
  }
  const partial = text.slice(prevIndex)
  transformedText += partial.replace(URL_REGEX, match => `[${match}](${match})`)

  return transformedText
}
<commit_msg>Updating URL regex to be more comprehensive.<commit_after>import * as _collections from './collections'

// Source: https://gist.github.com/dperini/729294
const URL_REGEX = /(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[\/?#]\S*)?/i

export const typeSafeEq = (x, y) => x.toString() === y.toString()
export const orStr = e => e || ''
export const orArr = e => e || []

const escSpecialChars = str => str.replace(/\$|\{|\}/g, e => `\\${e}`)
const toSource = re => re instanceof RegExp ? re.source : escSpecialChars(re)
const parenthesize = str => `(?:${str})`
export function or(res) {
  const strParts = res.map(toSource).filter(_collections.isPresent).map(parenthesize)
  const lengthSorted = _.sortBy(strParts, prt => -prt.length) // To avoid partial replacements.
  return new RegExp(parenthesize(lengthSorted.join('|')), 'g')
}

export function replaceByMap(str, replacementMap) {
  const regex = or(Object.keys(replacementMap))
  return str.replace(regex, match => replacementMap[match])
}

export function makeURLsMarkdown(text) {
  const fullRegex = new RegExp(`\\[.+\\]\\(${URL_REGEX.source}\\)`, 'g')
  let transformedText = ''
  let prevIndex = 0
  let matchArr
  while ((matchArr = fullRegex.exec(text)) !== null) {
    const partial = text.slice(prevIndex, matchArr.index)
    transformedText += partial.replace(URL_REGEX, match => `[${match}](${match})`)
    transformedText += matchArr[0]
    prevIndex = matchArr.index + matchArr[0].length
  }
  const partial = text.slice(prevIndex)
  transformedText += partial.replace(URL_REGEX, match => `[${match}](${match})`)

  return transformedText
}
<|endoftext|>