Repository: sjbarag/brs
Pull request #342 (2019-11-15 01:47:50)

Adding functions into custom component scopes
This PR covers:

1. Adding individual environment scopes to custom components
2. Parsing `<script>` tags to build a tree of functions split by scope
3. Attaching these scopes to the corresponding component's environment

Note that this does not cover supporting multiple `init()` functions. This will come in the next iteration.

sjbarag: oh dang is it time to finally create a `scenegraph` label in this project?! 

![wow](https://media.giphy.com/media/vQqeT3AYg8S5O/giphy.gif)
sjbarag: Also, please note that me merging #339 earlier this morning caused a merge conflict!  Shouldn't be too difficult to resolve though 😉 
MATATAT: Have updated a decent amount based on feedback. I've tried to separate each commit out into individual concerns to try and help match to feedback and hopefully make re-review quicker.

Patch:
diff --git a/src/Error.ts b/src/Error.ts
index 0591503..a7e0865 100644
--- a/src/Error.ts
+++ b/src/Error.ts
@@ -76,6 +76,23 @@ export class TypeMismatch extends BrsError {
     }
 }
 
+/**
+ * Logs a detected BRS error to console.
+ * @param err the error to log to console
+ */
+export function logConsoleError(err: BrsError) {
+    console.error(err.format());
+}
+
+/**
+ * Produces a function that writes errors to the given error stream.
+ * @param errorStream write stream to write errors to.
+ * @returns function that writes to given write stream.
+ */
+export function getLoggerUsing(errorStream: NodeJS.WriteStream): (err: BrsError) => boolean {
+    return err => errorStream.write(err.format());
+}
+
 /**
  * Returns the `.kind` property of a `BrsType`, otherwise returns the provided `ValueKind`.
  * @param maybeType the `BrsType` to extract a `.kind` field from, or the `ValueKind` to return directly
diff --git a/src/LexerParser.ts b/src/LexerParser.ts
new file mode 100644
index 0000000..4e88304
--- /dev/null
+++ b/src/LexerParser.ts
@@ -0,0 +1,80 @@
+import * as fs from "fs";
+import { promisify } from "util";
+import pSettle from "p-settle";
+const readFile = promisify(fs.readFile);
+
+import { Lexer } from "./lexer";
+import { Parser, Stmt } from "./parser";
+import * as PP from "./preprocessor";
+
+import * as BrsTypes from "./brsTypes";
+export { BrsTypes as types };
+export { PP as preprocessor };
+import { ManifestValue } from "./preprocessor/Manifest";
+import * as BrsError from "./Error";
+import { defaultExecutionOptions, ExecutionOptions } from "./interpreter";
+
+export function getLexerParserFn(
+    manifest: Map<string, ManifestValue>,
+    options: Partial<ExecutionOptions>
+) {
+    const executionOptions = Object.assign(defaultExecutionOptions, options);
+    return async function parse(filenames: string[]): Promise<Stmt.Statement[]> {
+        let parsedFiles = await pSettle(
+            filenames.map(async filename => {
+                let contents;
+                try {
+                    contents = await readFile(filename, "utf-8");
+                } catch (err) {
+                    return Promise.reject({
+                        message: `brs: can't open file '${filename}': [Errno ${err.errno}]`,
+                    });
+                }
+
+                let lexer = new Lexer();
+                let preprocessor = new PP.Preprocessor();
+                let parser = new Parser();
+                [lexer, preprocessor, parser].forEach(emitter =>
+                    emitter.onError(BrsError.getLoggerUsing(executionOptions.stderr))
+                );
+
+                let scanResults = lexer.scan(contents, filename);
+                if (scanResults.errors.length > 0) {
+                    return Promise.reject({
+                        message: "Error occurred during lexing",
+                    });
+                }
+
+                let preprocessResults = preprocessor.preprocess(scanResults.tokens, manifest);
+                if (preprocessResults.errors.length > 0) {
+                    return Promise.reject({
+                        message: "Error occurred during pre-processing",
+                    });
+                }
+
+                let parseResults = parser.parse(preprocessResults.processedTokens);
+                if (parseResults.errors.length > 0) {
+                    return Promise.reject({
+                        message: "Error occurred parsing",
+                    });
+                }
+
+                return Promise.resolve(parseResults.statements);
+            })
+        );
+
+        // don't execute anything if there were reading, lexing, or parsing errors
+        if (parsedFiles.some(file => file.isRejected)) {
+            return Promise.reject({
+                messages: parsedFiles
+                    .filter(file => file.isRejected)
+                    .map(rejection => rejection.reason.message),
+            });
+        }
+
+        // combine statements from all files into one array
+        return parsedFiles
+            .map(file => file.value || [])
+            .reduce((allStatements, fileStatements) => [...allStatements, ...fileStatements], []);
+    };
+}
diff --git a/src/componentprocessor/index.ts b/src/componentprocessor/index.ts
index 83fe08e..d50c6f8 100644
--- a/src/componentprocessor/index.ts
+++ b/src/componentprocessor/index.ts
@@ -5,6 +5,7 @@ import { XmlDocument, XmlElement } from "xmldoc";
 import pSettle = require("p-settle");
 const readFile = promisify(fs.readFile);
 import * as fg from "fast-glob";
+import { Environment } from "../interpreter/Environment";
 
 interface FieldAttributes {
     id: string;
@@ -29,6 +30,11 @@ interface ComponentNode {
     children: ComponentNode[];
 }
 
+export interface ComponentScript {
+    type: string;
+    uri: string;
+}
+
 export class ComponentDefinition {
     public contents?: string;
     public xmlNode?: XmlDocument;
@@ -38,6 +44,8 @@ export class ComponentDefinition {
     public processed: boolean = false;
     public fields: ComponentField = {};
     public children: ComponentNode[] = [];
+    public scripts: ComponentScript[] = [];
+    public environment: Environment | undefined;
 
     constructor(readonly xmlPath: string) {}
 
@@ -129,11 +137,13 @@ async function processXmlTree(
         let xmlNode = nodeDef.xmlNode;
         if (xmlNode) {
             nodeDef.children = getChildren(xmlNode);
+            nodeDef.scripts = getScripts(xmlNode);
             let baseNode = xmlNode.attr.extends;
             while (baseNode) {
                 let baseNodeDef = nodeDefMap.get(baseNode);
                 if (baseNodeDef) {
                     nodeDef.children = [...getChildren(baseNodeDef.xmlNode!), ...nodeDef.children];
+                    nodeDef.scripts = [...getScripts(baseNodeDef.xmlNode!), ...nodeDef.scripts];
                     baseNode = baseNodeDef.xmlNode!.attr.extends;
                 }
             }
@@ -213,3 +223,20 @@ function parseChildren(element: XmlElement, children: ComponentNode[]): void {
         children.push(childComponent);
     });
 }
+
+function getScripts(node: XmlDocument): ComponentScript[] {
+    let scripts = node.childrenNamed("script");
+    let componentScripts: ComponentScript[] = [];
+
+    // TODO: Verify if uri is valid
+    scripts.map(script => {
+        if (script.attr) {
+            componentScripts.push({
+                type: script.attr.type,
+                uri: script.attr.uri,
+            });
+        }
+    });
+
+    return componentScripts;
+}
diff --git a/src/index.ts b/src/index.ts
index fd8c38e..0b5a986 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,15 +1,17 @@
 import * as fs from "fs";
 import * as readline from "readline";
-import { promisify } from "util";
-import pSettle from "p-settle";
-const readFile = promisify(fs.readFile);
 
 import { Lexer } from "./lexer";
 import * as PP from "./preprocessor";
-import { getComponentDefinitionMap } from "./componentprocessor";
+import {
+    getComponentDefinitionMap,
+    ComponentDefinition,
+    ComponentScript,
+} from "./componentprocessor";
 import { Parser } from "./parser";
 import { Interpreter, ExecutionOptions, defaultExecutionOptions } from "./interpreter";
 import * as BrsError from "./Error";
+import * as LexerParser from "./LexerParser";
 
 import * as _lexer from "./lexer";
 export { _lexer as lexer };
@@ -18,6 +20,8 @@ export { BrsTypes as types };
 export { PP as preprocessor };
 import * as _parser from "./parser";
 export { _parser as parser };
+import { URL } from "url";
+import * as path from "path";
 
 /**
  * Executes a BrightScript file by path and writes its output to the streams
@@ -34,73 +38,36 @@ export async function execute(filenames: string[], options: Partial<ExecutionOpt
     const executionOptions = Object.assign(defaultExecutionOptions, options);
 
     let manifest = await PP.getManifest(executionOptions.root);
+    let componentDefinitions = await getComponentDefinitionMap(executionOptions.root);
+
+    componentDefinitions.forEach((component: ComponentDefinition) => {
+        if (component.scripts.length < 1) return;
+        try {
+            component.scripts = component.scripts.map((script: ComponentScript) => {
+                script.uri = path.join(
+                    options.root ? options.root : __dirname,
+                    new URL(script.uri).pathname
+                );
+                return script;
+            });
+        } catch (error) {
+            throw new Error(
+                `Encountered an error when parsing component ${component.name}: ${error}`
+            );
+        }
+    });
 
-    let nodeDefs = await getComponentDefinitionMap(executionOptions.root);
-
-    // wait for all files to be read, lexed, and parsed, but don't exit on the first error
-    let parsedFiles = await pSettle(
-        filenames.map(async filename => {
-            let contents;
-            try {
-                contents = await readFile(filename, "utf-8");
-            } catch (err) {
-                return Promise.reject({
-                    message: `brs: can't open file '${filename}': [Errno ${err.errno}]`,
-                });
-            }
-
-            let lexer = new Lexer();
-            let preprocessor = new PP.Preprocessor();
-            let parser = new Parser();
-            [lexer, preprocessor, parser].forEach(emitter => emitter.onError(logError));
-
-            let scanResults = lexer.scan(contents, filename);
-            if (scanResults.errors.length > 0) {
-                return Promise.reject({
-                    message: "Error occurred during lexing",
-                });
-            }
-
-            let preprocessResults = preprocessor.preprocess(scanResults.tokens, manifest);
-            if (preprocessResults.errors.length > 0) {
-                return Promise.reject({
-                    message: "Error occurred during pre-processing",
-                });
-            }
-
-            let parseResults = parser.parse(preprocessResults.processedTokens);
-            if (parseResults.errors.length > 0) {
-                return Promise.reject({
-                    message: "Error occurred parsing",
-                });
-            }
-
-            return Promise.resolve(parseResults.statements);
-        })
+    let lexerParserFn = LexerParser.getLexerParserFn(manifest, options);
+    const interpreter = await Interpreter.withSubEnvsFromComponents(
+        componentDefinitions,
+        lexerParserFn
     );
-
-    // don't execute anything if there were reading, lexing, or parsing errors
-    if (parsedFiles.some(file => file.isRejected)) {
-        return Promise.reject({
-            messages: parsedFiles
-                .filter(file => file.isRejected)
-                .map(rejection => rejection.reason.message),
-        });
+    if (!interpreter) {
+        throw new Error("Unable to build interpreter.");
     }
 
-    // combine statements from all files into one array
-    let statements = parsedFiles
-        .map(file => file.value || [])
-        .reduce((allStatements, fileStatements) => [...allStatements, ...fileStatements], []);
-
-    // execute them
-    const interpreter = new Interpreter(executionOptions);
-    interpreter.onError(logError);
-    // save each custom component def into a global map so we can access it
-    // at run time when we call `createObjectByType`
-    interpreter.environment.nodeDefMap = nodeDefs;
-
-    return interpreter.exec(statements);
+    let mainStatements = await lexerParserFn(filenames);
+    return interpreter.exec(mainStatements);
 }
 
 /**
@@ -136,7 +103,7 @@ export function lexParseSync(filenames: string[], options: Partial<ExecutionOpti
  */
 export function repl() {
     const replInterpreter = new Interpreter();
-    replInterpreter.onError(logError);
+    replInterpreter.onError(BrsError.getLoggerUsing(process.stderr));
 
     const rl = readline.createInterface({
         input: process.stdin,
@@ -179,9 +146,10 @@ function run(
 ) {
     const lexer = new Lexer();
     const parser = new Parser();
+    const logErrorFn = BrsError.getLoggerUsing(options.stderr);
 
-    lexer.onError(logError);
-    parser.onError(logError);
+    lexer.onError(logErrorFn);
+    parser.onError(logErrorFn);
 
     const scanResults = lexer.scan(contents, "REPL");
     if (scanResults.errors.length > 0) {
@@ -204,11 +172,3 @@ function run(
         return;
     }
 }
-
-/**
- * Logs a detected BRS error to stderr.
- * @param err the error to log to `stderr`
- */
-function logError(err: BrsError.BrsError) {
-    console.error(err.format());
-}
diff --git a/src/interpreter/Environment.ts b/src/interpreter/Environment.ts
index 63c21f6..c0c56da 100644
--- a/src/interpreter/Environment.ts
+++ b/src/interpreter/Environment.ts
@@ -201,13 +201,17 @@ export class Environment {
      * 3. Parameters passed into the function
      * 4. The `m` pointer, defined by the way in which a function was called
      * 5. Currently focused node object that reacts to onKey button presses
+     * @param includeModuleScope whether or not to includer caller's module scope into
+     * the cloned environment.
      *
      * @returns a copy of this environment but with no function-scoped values.
      */
-    public createSubEnvironment(): Environment {
+    public createSubEnvironment(includeModuleScope: boolean = true): Environment {
         let newEnvironment = new Environment(this.rootM);
         newEnvironment.global = new Map(this.global);
-        newEnvironment.module = new Map(this.module);
+        newEnvironment.module = includeModuleScope
+            ? new Map(this.module)
+            : new Map<string, BrsType>();
         newEnvironment.mPointer = this.mPointer;
         newEnvironment.mockObjects = this.mockObjects;
         newEnvironment.focusedNode = this.focusedNode;
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 9f6e0bb..32e1fa2 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -18,14 +18,13 @@ import {
     MismatchReason,
     Callable,
     BrsNumber,
-    Comparable,
     Float,
 } from "../brsTypes";
 
 import { Lexeme } from "../lexer";
 import { isToken } from "../lexer/Token";
 import { Expr, Stmt } from "../parser";
-import { BrsError, TypeMismatch } from "../Error";
+import { BrsError, TypeMismatch, getLoggerUsing } from "../Error";
 
 import * as StdLib from "../stdlib";
 import { _brs_ } from "../extensions";
@@ -39,6 +38,10 @@ import MemoryFileSystem from "memory-fs";
 import { BrsComponent } from "../brsTypes/components/BrsComponent";
 import { isBoxable, isUnboxable } from "../brsTypes/Boxing";
 
+import { ManifestValue } from "../preprocessor/Manifest";
+import { ComponentDefinition } from "../componentprocessor";
+import pSettle from "p-settle";
+
 /** The set of options used to configure an interpreter's execution. */
 export interface ExecutionOptions {
     /** The base path for  */
@@ -94,6 +97,40 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
         this.events.once("err", errorHandler);
     }
 
+    /**
+     * Builds out all the sub-environments for the given components. Components are saved into the calling interpreter
+     * instance. This function will mutate the state of the calling interpreter.
+     * @param componentMap Map of all components to be assigned to this interpreter
+     * @param parseFn Function used to parse components into interpretable statements
+     * @param options
+     */
+    public static async withSubEnvsFromComponents(
+        componentMap: Map<string, ComponentDefinition>,
+        parseFn: (filenames: string[]) => Promise<Stmt.Statement[]>,
+        options: ExecutionOptions = defaultExecutionOptions
+    ) {
+        let interpreter = new Interpreter(options);
+        interpreter.onError(getLoggerUsing(options.stderr));
+
+        interpreter.environment.nodeDefMap = componentMap;
+        await pSettle(
+            Array.from(componentMap).map(async componentKV => {
+                let [_, component] = componentKV;
+                component.environment = interpreter.environment.createSubEnvironment(
+                    /* includeModuleScope */ false
+                );
+                let statements = await parseFn(component.scripts.map(c => c.uri));
+                interpreter.inSubEnv(subInterpreter => {
+                    subInterpreter.environment.setM(interpreter.environment.getM());
+                    subInterpreter.exec(statements);
+                    return BrsInvalid.Instance;
+                }, component.environment);
+            })
+        );
+
+        return interpreter;
+    }
+
     /**
      * Creates a new Interpreter, including any global properties and functions.
      * @param options configuration for the execution, including the streams to use for `stdout` and
@@ -126,10 +163,12 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
      * passes the sub-interpreter to the provided JavaScript function. Always
      * reverts the current interpreter's environment to its original value.
      * @param func the JavaScript function to execute with the sub interpreter.
+     * @param environment (Optional) the environment to run the interpreter in.
      */
-    inSubEnv(func: (interpreter: Interpreter) => BrsType): BrsType {
+    inSubEnv(func: (interpreter: Interpreter) => BrsType, environment?: Environment): BrsType {
         let originalEnvironment = this._environment;
-        let newEnv = this._environment.createSubEnvironment();
+        let newEnv = environment || this._environment.createSubEnvironment();
+
         try {
             this._environment = newEnv;
             return func(this);


Test patch:
diff --git a/test/componentprocessor/componentprocessor.test.js b/test/componentprocessor/componentprocessor.test.js
index bad7827..de92749 100644
--- a/test/componentprocessor/componentprocessor.test.js
+++ b/test/componentprocessor/componentprocessor.test.js
@@ -1,10 +1,13 @@
 const { getComponentDefinitionMap, ComponentDefinition } = require("../../lib/componentprocessor");
+const path = require("path");
 
 jest.mock("fast-glob");
 jest.mock("fs");
 const fg = require("fast-glob");
 const fs = require("fs");
 
+const realFs = jest.requireActual("fs");
+
 describe.only("component parsing support", () => {
     afterEach(() => {
         fg.sync.mockRestore();
@@ -53,42 +56,55 @@ describe.only("component parsing support", () => {
             expect(xmlNode.attr.extends).toEqual("BaseComponent");
         });
 
-        it("parses children nodes in correct order", async () => {
-            const baseComp = `
-<?xml version="1.0" encoding="utf-8" ?>
-<component name="BaseComponent">
-    <children>
-        <Label name="label_a" />
-    </children>
-</component>
-            `;
-            const extendedComp = `
-<?xml version="1.0" encoding="utf-8" ?>
-<component name="ExtendedComponent" extends="BaseComponent">
-    <children>
-            <Label name="label_b" />
-    </children>
-</component>
-            `;
-
-            fg.sync.mockImplementation(() => {
-                return ["base_component.xml", "extended_component.xml"];
+        describe("parsing expectations", () => {
+            beforeEach(() => {
+                fg.sync.mockImplementation(() => {
+                    return [
+                        "baseComponent.xml",
+                        "extendedComponent.xml",
+                        "scripts/baseComp.brs",
+                        "scripts/extendedComp.brs",
+                        "scripts/utility.brs",
+                    ];
+                });
+                fs.readFile.mockImplementation((filename, _, cb) => {
+                    resourcePath = path.join(__dirname, "resources", filename);
+                    realFs.readFile(resourcePath, (err, contents) => {
+                        cb(/* no error */ null, contents);
+                    });
+                });
             });
-            fs.readFile.mockImplementation((filename, _, cb) => {
-                selected = filename.includes("base_component.xml") ? baseComp : extendedComp;
-                cb(/* no error */ null, selected);
+
+            it("parses children nodes in correct order", async () => {
+                let map = await getComponentDefinitionMap("/doesnt/matter");
+                let parsedExtendedComp = map.get("ExtendedComponent");
+                expect(parsedExtendedComp).not.toBeUndefined();
+                expect(parsedExtendedComp.children).not.toBeUndefined();
+                expect(parsedExtendedComp.children.length).toBeGreaterThan(0);
+
+                let expectedChildOrder = ["label_a", "label_b"];
+                let childOrder = parsedExtendedComp.children.map(child => {
+                    return child.fields && child.fields.name;
+                });
+                expect(childOrder).toEqual(expectedChildOrder);
             });
 
-            let map = await getComponentDefinitionMap("/doesnt/matter");
-            let parsedExtendedComp = map.get("ExtendedComponent");
-            expect(parsedExtendedComp).not.toBeUndefined();
-            expect(parsedExtendedComp.children).not.toBeUndefined();
-            expect(parsedExtendedComp.children.length).toBeGreaterThan(0);
-            let expectedChildOrder = ["label_a", "label_b"];
-            let childOrder = parsedExtendedComp.children.map(child => {
-                return child.fields && child.fields.name;
+            it("adds all scripts into node in correct order", async () => {
+                let map = await getComponentDefinitionMap("/doesnt/matter");
+                let parsedExtendedComp = map.get("ExtendedComponent");
+                expect(parsedExtendedComp).not.toBeUndefined();
+                expect(parsedExtendedComp.scripts).not.toBeUndefined();
+                expect(parsedExtendedComp.scripts.length).toBeGreaterThan(0);
+
+                const expectedPrefix = "pkg:/test/componentprocessor/resources/scripts/";
+                let expectedScripts = [
+                    "baseComponent.brs",
+                    "extendedComponent.brs",
+                    "utility.brs",
+                ].map(scriptName => path.join(expectedPrefix, scriptName));
+                let actualScripts = parsedExtendedComp.scripts.map(script => script.uri);
+                expect(actualScripts).toEqual(expectedScripts);
             });
-            expect(childOrder).toEqual(expectedChildOrder);
         });
     });
 });
diff --git a/test/componentprocessor/resources/baseComponent.xml b/test/componentprocessor/resources/baseComponent.xml
new file mode 100644
index 0000000..7ca30a2
--- /dev/null
+++ b/test/componentprocessor/resources/baseComponent.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<component name="BaseComponent">
+<script type="text/brightscript" uri="pkg:/test/componentprocessor/resources/scripts/baseComponent.brs" />
+<children>
+    <Label name="label_a" />
+</children>
+</component>
diff --git a/test/componentprocessor/resources/extendedComponent.xml b/test/componentprocessor/resources/extendedComponent.xml
new file mode 100644
index 0000000..604769e
--- /dev/null
+++ b/test/componentprocessor/resources/extendedComponent.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<component name="ExtendedComponent" extends="BaseComponent">
+<script type="text/brightscript" uri="pkg:/test/componentprocessor/resources/scripts/extendedComponent.brs" />
+<script type="text/brightscript" uri="pkg:/test/componentprocessor/resources/scripts/utility.brs" />
+<children>
+        <Label name="label_b" />
+</children>
+</component>
diff --git a/test/componentprocessor/resources/scripts/baseComponent.brs b/test/componentprocessor/resources/scripts/baseComponent.brs
new file mode 100644
index 0000000..6cf230a
--- /dev/null
+++ b/test/componentprocessor/resources/scripts/baseComponent.brs
@@ -0,0 +1,4 @@
+sub init()
+    ?"base component"
+    x = 5
+end sub
\ No newline at end of file
diff --git a/test/componentprocessor/resources/scripts/extendedComponent.brs b/test/componentprocessor/resources/scripts/extendedComponent.brs
new file mode 100644
index 0000000..8db7136
--- /dev/null
+++ b/test/componentprocessor/resources/scripts/extendedComponent.brs
@@ -0,0 +1,4 @@
+sub init()
+    ?"extended component"
+    x = 10
+end sub
\ No newline at end of file
diff --git a/test/componentprocessor/resources/scripts/utility.brs b/test/componentprocessor/resources/scripts/utility.brs
new file mode 100644
index 0000000..3f31435
--- /dev/null
+++ b/test/componentprocessor/resources/scripts/utility.brs
@@ -0,0 +1,3 @@
+sub utility()
+    ?"utility"
+end sub
\ No newline at end of file
diff --git a/test/e2e/ConditionalCompilation.test.js b/test/e2e/ConditionalCompilation.test.js
index 2d56cfc..9ec8bd5 100644
--- a/test/e2e/ConditionalCompilation.test.js
+++ b/test/e2e/ConditionalCompilation.test.js
@@ -31,8 +31,7 @@ describe("end to end conditional compilation", () => {
 
     describe("(with sterr captured)", () => {
         test("conditional-compilation/compile-error.brs", async done => {
-            // make console.error empty so we don't clutter test output
-            let stderr = jest.spyOn(console, "error").mockImplementation(() => {});
+            let stderr = outputStreams.stderrSpy;
 
             try {
                 await execute(
diff --git a/test/e2e/E2ETests.js b/test/e2e/E2ETests.js
index 559172b..d80eabd 100644
--- a/test/e2e/E2ETests.js
+++ b/test/e2e/E2ETests.js
@@ -23,8 +23,11 @@ exports.allArgs = function(jestMock) {
 exports.createMockStreams = function() {
     const stdout = Object.assign(new stream.PassThrough(), process.stdout);
     const stderr = Object.assign(new stream.PassThrough(), process.stderr);
-    jest.spyOn(stdout, "write").mockImplementation(() => {});
-    jest.spyOn(stderr, "write").mockImplementation(() => {});
 
-    return { stdout, stderr };
+    return {
+        stdout,
+        stderr,
+        stdoutSpy: jest.spyOn(stdout, "write").mockImplementation(() => {}),
+        stderrSpy: jest.spyOn(stderr, "write").mockImplementation(() => {}),
+    };
 };
diff --git a/test/e2e/TypeChecking.test.js b/test/e2e/TypeChecking.test.js
index 2bdc7e7..71d6c6a 100644
--- a/test/e2e/TypeChecking.test.js
+++ b/test/e2e/TypeChecking.test.js
@@ -3,12 +3,11 @@ const { execute } = require("../../lib/");
 const { createMockStreams, resourceFile, allArgs } = require("./E2ETests");
 
 describe("function argument type checking", () => {
-    let outputStreams;
+    let outputStreams, stderr;
 
     beforeAll(() => {
-        // make console.error empty so we don't clutter test output
-        stderr = jest.spyOn(console, "error").mockImplementation(() => {});
         outputStreams = createMockStreams();
+        stderr = outputStreams.stderrSpy;
     });
 
     afterEach(() => {
diff --git a/test/e2e/resources/components/AdvancedWidget.xml b/test/e2e/resources/components/AdvancedWidget.xml
index c748bb5..2c0dd41 100644
--- a/test/e2e/resources/components/AdvancedWidget.xml
+++ b/test/e2e/resources/components/AdvancedWidget.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <component name="AdvancedWidget" extends="NormalWidget">
+    <script type="text/brightscript" uri="pkg:/components/scripts/AdvancedWidget.brs" />
+    <script type="text/brightscript" uri="pkg:/components/scripts/SupportScript.brs" />
     <interface>
         <!-- output -->
         <field id="advancedStringField" type="string" value="advancedField!" alwaysNotify="true" />
diff --git a/test/e2e/resources/components/BaseWidget.xml b/test/e2e/resources/components/BaseWidget.xml
index 4edabec..b609aff 100644
--- a/test/e2e/resources/components/BaseWidget.xml
+++ b/test/e2e/resources/components/BaseWidget.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <component name="BaseWidget">
+    <script type="text/brightscript" uri="pkg:/components/scripts/BaseWidget.brs" />
     <interface>
         <field id="baseStringField" type="string" />
         <field id="baseIntField" type="integer" />
diff --git a/test/e2e/resources/components/NormalWidget.xml b/test/e2e/resources/components/NormalWidget.xml
index 020524d..fccc640 100644
--- a/test/e2e/resources/components/NormalWidget.xml
+++ b/test/e2e/resources/components/NormalWidget.xml
@@ -1,13 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <component name="NormalWidget" extends="BaseWidget">
-    <script type="text/brightscript" uri="pkg:/source/hello.brs" />
+    <script type="text/brightscript" uri="pkg:/components/scripts/NormalWidget.brs" />
     <interface>
         <field id="normalStringField" type="string" onChange="onNormalStringFieldChange" />
         <field id="normalIntField" type="integer" value="-321" />
         <field id="normalBoolField" type="boolean" value="true" />
     </interface>
-    <script type="text/brightscript" uri="pkg:/components/player/liveGuide/scripts/BaseGuideView.brs" />
-    <script type="text/brightscript" uri="pkg:/components/player/liveGuide/scripts/LiveGuideSettings.brs" />
     <children>
         <Label id="normalLabel" />
         <SimpleLabel id="normalSimpleLabel" />
diff --git a/test/e2e/resources/components/scripts/AdvancedWidget.brs b/test/e2e/resources/components/scripts/AdvancedWidget.brs
new file mode 100644
index 0000000..7f3fb8d
--- /dev/null
+++ b/test/e2e/resources/components/scripts/AdvancedWidget.brs
@@ -0,0 +1,2 @@
+sub init()
+end sub
\ No newline at end of file
diff --git a/test/e2e/resources/components/scripts/BaseWidget.brs b/test/e2e/resources/components/scripts/BaseWidget.brs
new file mode 100644
index 0000000..b3d813c
--- /dev/null
+++ b/test/e2e/resources/components/scripts/BaseWidget.brs
@@ -0,0 +1,3 @@
+' TODO: Uncomment when init logic has been implemented
+' sub init()
+' end sub
\ No newline at end of file
diff --git a/test/e2e/resources/components/scripts/NormalWidget.brs b/test/e2e/resources/components/scripts/NormalWidget.brs
new file mode 100644
index 0000000..b3d813c
--- /dev/null
+++ b/test/e2e/resources/components/scripts/NormalWidget.brs
@@ -0,0 +1,3 @@
+' TODO: Uncomment when init logic has been implemented
+' sub init()
+' end sub
\ No newline at end of file
diff --git a/test/e2e/resources/components/scripts/SupportScript.brs b/test/e2e/resources/components/scripts/SupportScript.brs
new file mode 100644
index 0000000..d956fe9
--- /dev/null
+++ b/test/e2e/resources/components/scripts/SupportScript.brs
@@ -0,0 +1,2 @@
+sub supportFunction()
+end sub
\ No newline at end of file
diff --git a/test/interpreter/Interpreter.test.js b/test/interpreter/Interpreter.test.js
new file mode 100644
index 0000000..fcce151
--- /dev/null
+++ b/test/interpreter/Interpreter.test.js
@@ -0,0 +1,63 @@
+const { getComponentDefinitionMap } = require("../../lib/componentprocessor");
+const { Interpreter, defaultExecutionOptions } = require("../../lib/interpreter");
+const path = require("path");
+const LexerParser = require("../../lib/LexerParser");
+
+jest.mock("fast-glob");
+jest.mock("fs");
+const fg = require("fast-glob");
+const fs = require("fs");
+
+const realFs = jest.requireActual("fs");
+
+describe("integration tests", () => {
+    beforeEach(() => {
+        fg.sync.mockImplementation(() => {
+            return [
+                "baseComponent.xml",
+                "extendedComponent.xml",
+                "scripts/baseComp.brs",
+                "scripts/extendedComp.brs",
+                "scripts/utility.brs",
+            ];
+        });
+        fs.readFile.mockImplementation((filename, _, cb) => {
+            resourcePath = path.join(__dirname, "resources", filename);
+            realFs.readFile(resourcePath, "utf8", (err, contents) => {
+                cb(/* no error */ null, contents);
+            });
+        });
+    });
+
+    afterEach(() => {
+        fg.sync.mockRestore();
+        fs.readFile.mockRestore();
+    });
+
+    test("interpreter environments have the right functions per component", async () => {
+        let componentMap = await getComponentDefinitionMap("/doesnt/matter");
+        componentMap.forEach(comp => {
+            comp.scripts = comp.scripts.map(script => {
+                script.uri = path.join("scripts/", path.parse(script.uri).base);
+                return script;
+            });
+        });
+        let interpreter = await Interpreter.withSubEnvsFromComponents(
+            componentMap,
+            LexerParser.getLexerParserFn(new Map(), defaultExecutionOptions)
+        );
+
+        let baseComp = interpreter.environment.nodeDefMap.get("BaseComponent");
+        expect(baseComp).not.toBeUndefined();
+        expect(baseComp.environment.module.size).toEqual(0);
+
+        let extendedComp = interpreter.environment.nodeDefMap.get("ExtendedComponent");
+        expect(extendedComp).not.toBeUndefined();
+        let actualFns = Array.from(extendedComp.environment.module).map(methodKV => {
+            let [fnName] = methodKV;
+            return fnName;
+        });
+
+        expect(actualFns).toEqual(["init", "test1", "utility"]);
+    });
+});
diff --git a/test/interpreter/resources/baseComponent.xml b/test/interpreter/resources/baseComponent.xml
new file mode 100644
index 0000000..7ca30a2
--- /dev/null
+++ b/test/interpreter/resources/baseComponent.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<component name="BaseComponent">
+<script type="text/brightscript" uri="pkg:/test/componentprocessor/resources/scripts/baseComponent.brs" />
+<children>
+    <Label name="label_a" />
+</children>
+</component>
diff --git a/test/interpreter/resources/extendedComponent.xml b/test/interpreter/resources/extendedComponent.xml
new file mode 100644
index 0000000..604769e
--- /dev/null
+++ b/test/interpreter/resources/extendedComponent.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<component name="ExtendedComponent" extends="BaseComponent">
+<script type="text/brightscript" uri="pkg:/test/componentprocessor/resources/scripts/extendedComponent.brs" />
+<script type="text/brightscript" uri="pkg:/test/componentprocessor/resources/scripts/utility.brs" />
+<children>
+        <Label name="label_b" />
+</children>
+</component>
diff --git a/test/interpreter/resources/scripts/baseComponent.brs b/test/interpreter/resources/scripts/baseComponent.brs
new file mode 100644
index 0000000..539c3b7
--- /dev/null
+++ b/test/interpreter/resources/scripts/baseComponent.brs
@@ -0,0 +1,5 @@
+' TODO: Uncomment when init logic has been implemented
+' sub init()
+'     ?"base component"
+'     x = 5
+' end sub
diff --git a/test/interpreter/resources/scripts/extendedComponent.brs b/test/interpreter/resources/scripts/extendedComponent.brs
new file mode 100644
index 0000000..f08d963
--- /dev/null
+++ b/test/interpreter/resources/scripts/extendedComponent.brs
@@ -0,0 +1,8 @@
+sub init()
+    ?"extended component"
+    x = 10
+end sub
+
+sub test1()
+    y = 100
+end sub
diff --git a/test/interpreter/resources/scripts/utility.brs b/test/interpreter/resources/scripts/utility.brs
new file mode 100644
index 0000000..4d553a5
--- /dev/null
+++ b/test/interpreter/resources/scripts/utility.brs
@@ -0,0 +1,3 @@
+sub utility()
+    ?"utility"
+end sub