jsinstrument 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jsinstrument.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mushan Yang
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # JSInstrument
2
+
3
+ Instrument JavaScript source code to monitor code coverage in pure Ruby
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'jsinstrument'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install jsinstrument
18
+
19
+ ## Usage
20
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jsinstrument/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jsinstrument"
8
+ spec.version = JSInstrument::VERSION
9
+ spec.authors = ["Mushan Yang"]
10
+ spec.email = ["mushan.yang@hulu.com"]
11
+ spec.description = %q{Instrument JavaScript source code to monitor code coverage in pure Ruby}
12
+ spec.summary = %q{Instrument JavaScript source code to monitor code coverage in pure Ruby}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rkelly", "~>1.0.7"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
@@ -0,0 +1,2 @@
1
+ require "jsinstrument/version"
2
+ require "jsinstrument/instrumenter"
@@ -0,0 +1,135 @@
1
+ /* automatically generated by JSInstrument */
2
+ /*
3
+ * create __coverage__ object as a global variable
4
+ */
5
+ !function(root){
6
+ if (root && (typeof root.%{js_object_name} === 'undefined')) {
7
+ root.%{js_object_name} = {
8
+ /**
9
+ * Coverage data
10
+ */
11
+ _data: {},
12
+ /**
13
+ * A thread is uploading data
14
+ */
15
+ _uploading: false,
16
+ /**
17
+ * mark code line
18
+ */
19
+ mark: function(file, lines) {
20
+ if(!this._data[file]) this._data[file] = [];
21
+ for (var i in lines) {
22
+ if(!this._data[file][lines[i]]) {
23
+ this._data[file][lines[i]] = 0;
24
+ }
25
+ }
26
+ },
27
+ /**
28
+ * Hit a line
29
+ */
30
+ hit: function(file, line) {
31
+ if(!this._data[file]) this._data[file] = [];
32
+
33
+ if(!this._data[file][line]) this._data[file][line] = 1;
34
+ else this._data[file][line] = this._data[file][line] + 1;
35
+ },
36
+ /**
37
+ * Push data to server
38
+ */
39
+ upload: function(force, mark) {
40
+ /**
41
+ * Avoid dup upload
42
+ */
43
+ if(!force && this._uploading) return;
44
+
45
+ this._uploading = true;
46
+
47
+ try {
48
+ $.post('%{upload_url}', this._serialize(this._data, mark)).success(function() {
49
+ for (var file in _$jscoverage._data) {
50
+ var length = _$jscoverage._data[file].length;
51
+ for (var line = 0; line < length; line++) {
52
+ var value = _$jscoverage._data[file][line];
53
+ if (value === undefined || value === null) continue;
54
+ _$jscoverage._data[file][line] = 0;
55
+ }
56
+ }
57
+ }).complete(function() {
58
+ _$jscoverage._uploading = false;
59
+ });
60
+ } catch (e) {
61
+ this._uploading = false;
62
+ }
63
+ }
64
+ /**
65
+ * serialize data for data uploading
66
+ */
67
+ _serialize: function(data, mark) {
68
+ var json = [];
69
+
70
+ for (var file in data) {
71
+ var coverage = data[file];
72
+
73
+ var array = [];
74
+ var length = coverage.length;
75
+ for (var line = 0; line < length; line++) {
76
+ var value = coverage[line];
77
+ if (value === undefined || value === null) {
78
+ value = '';
79
+ }
80
+ array.push(value);
81
+ }
82
+
83
+ json.push(this._quote(file) + ':[' + array.join(',') + ']');
84
+ }
85
+
86
+ var result = '{' + json.join(',') + '}';
87
+
88
+ var r = {
89
+ project: '%{project_name}',
90
+ commit: '%{commit_hash}',
91
+ batch: '%{batch_text}',
92
+ mark: mark,
93
+ data: result
94
+ }
95
+
96
+ var data_compress_method = %{data_compress_method};
97
+
98
+ if (data_compress_method) {
99
+ var result_deflated = data_compress_method(result);
100
+ var result_encoded = btoa(result_deflated);
101
+ r.data_compressed = true;
102
+ r.data = result_encoded;
103
+ }
104
+
105
+ return r;
106
+ },
107
+ _quote: function(s) {
108
+ return '"' + s.replace(/[\u0000-\u001f"\\\u007f-\uffff]/g, function (c) {
109
+ switch (c) {
110
+ case '\b': return '\\b';
111
+ case '\f': return '\\f';
112
+ case '\n': return '\\n';
113
+ case '\r': return '\\r';
114
+ case '\t': return '\\t';
115
+ case '"': return '\\"';
116
+ case '\\': return '\\\\';
117
+ default: return '\\u' + this._pad(c.charCodeAt(0).toString(16));
118
+ }
119
+ }) + '"';
120
+ },
121
+ _pad: function(s) {
122
+ return '0000'.substr(s.length) + s;
123
+ },
124
+ };
125
+
126
+ // $(document).ready(function() {
127
+ // $(window).unload(function (e) {
128
+ // _$jscoverage.upload(true);
129
+ // });
130
+ // });
131
+ }
132
+
133
+ root.%{js_object_name}.mark('%{src_filename}', %{instrumented_lines});
134
+ }(this);
135
+
@@ -0,0 +1,133 @@
1
+ require 'set'
2
+ require 'rkelly'
3
+ require 'rkelly/node'
4
+ require 'rkelly/parent_builder'
5
+ include RKelly::Nodes
6
+
7
+ module JSInstrument
8
+ class Instrumenter
9
+
10
+ attr_accessor :js_object_name, :upload_url, :project_name, :commit_hash, :batch_text, :data_compress_method
11
+
12
+ def js_object_name
13
+ @js_object_name || '__coverage__'
14
+ end
15
+
16
+ def instrument src, filename
17
+ raise 'Filename not set' unless filename
18
+ ast = RKelly::Parser.new.parse src
19
+ raise 'Parse source failed.' unless ast
20
+ ast, instrumented_lines = instrument_ast ast
21
+ instrumented_src = ast.to_ecma
22
+
23
+ inserting = File.open(File.dirname(__FILE__) + '/inserting.js').read
24
+
25
+ bindings = {
26
+ js_object_name: self.js_object_name,
27
+ upload_url: self.upload_url,
28
+ src_filename: self.filename,
29
+ project_name: self.project_name,
30
+ commit_hash: self.commit_hash,
31
+ batch_text: self.batch_text,
32
+ data_compress_method: self.data_compress_method,
33
+ instrumented_lines: instrumented_lines
34
+ }
35
+
36
+ (inserting % bindings) + instrumented_src
37
+ end
38
+
39
+ private
40
+ def instrument_ast ast
41
+ # build parent attr for all nodes in ast
42
+ #
43
+ ast = RKelly::Visitors::ParentBuilder.new.build ast
44
+ instrumented_lines = Set.new
45
+ ast.each do |node|
46
+ parent = node.parent
47
+ line = node.line
48
+ # instrument only if we can get the line no. and we haven't instrumented it
49
+ #
50
+ if line and not instrumented_lines.member? line
51
+ if ExpressionStatementNode === node or EmptyStatementNode === node or
52
+ DoWhileNode === node or WhileNode === node or ForNode === node or
53
+ ForInNode === node or ContinueNode === node or VarDeclNode === node or
54
+ SwitchNode === node or BreakNode === node or ThrowNode === node or
55
+ ReturnNode === node or IfNode === node or VarStatementNode === node
56
+
57
+ if DoWhileNode === parent
58
+ if parent.left.eql? node
59
+ instrumented_lines.add line
60
+ parent.left = get_inserted_block node
61
+ end
62
+ elsif IfNode === parent or WhileNode === parent or
63
+ ForNode === parent or ForInNode === parent or WithNode === parent
64
+ if parent.value.eql? node
65
+ instrumented_lines.add line
66
+ parent.value = get_inserted_block node
67
+ elsif parent.else.eql? node
68
+ instrumented_lines.add line
69
+ parent.else = get_inserted_block node
70
+ end
71
+ elsif CaseBlockNode === parent or LabelNode === parent
72
+ # do nothing here
73
+ elsif SourceElementsNode === parent
74
+ if insert_func_before node
75
+ instrumented_lines.add line
76
+ end
77
+ end
78
+ elsif WithNode === node or TryNode === node
79
+ if SourceElementsNode === parent
80
+ if insert_func_before node
81
+ instrumented_lines.add line
82
+ end
83
+ end
84
+ elsif FunctionDeclNode === node or FunctionExprNode === node
85
+ if insert_func_before node
86
+ instrumented_lines.add line
87
+ end
88
+ end
89
+ end
90
+ end
91
+ [ast, instrumented_lines.sort]
92
+ end
93
+
94
+ def get_insert_func_call line
95
+ ExpressionStatementNode.new(
96
+ FunctionCallNode.new(
97
+ ResolveNode.new("#{self.js_object_name}.hit"),
98
+ ArgumentsNode.new([
99
+ StringNode.new("\"#{self.src_filename}\""),
100
+ NumberNode.new(line)
101
+ ])
102
+ )
103
+ )
104
+ end
105
+
106
+ def get_inserted_block node
107
+ BlockNode.new(
108
+ SourceElementsNode.new([
109
+ get_insert_func_call(node.line),
110
+ node
111
+ ])
112
+ )
113
+ end
114
+
115
+ def insert_func_before node
116
+ # get the original node line
117
+ #
118
+ line = node.line
119
+ # backtrack until we can insert the func
120
+ #
121
+ while not SourceElementsNode === node.parent and node.parent != nil
122
+ node = node.parent
123
+ end
124
+ if node.parent == nil
125
+ false
126
+ else
127
+ index = node.parent.value.index{|x| x.eql? node}
128
+ node.parent.value.insert index, get_insert_func_call(line)
129
+ true
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,3 @@
1
+ module JSInstrument
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,14 @@
1
+ module RKelly
2
+ module Nodes
3
+ class Node
4
+ # add an accessor to receord the parent of a node
5
+ #
6
+ attr_accessor :parent
7
+ end
8
+ class IfNode
9
+ # make the else part of IfNode writable
10
+ #
11
+ attr_accessor :else
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,120 @@
1
+ module RKelly
2
+ module Visitors
3
+ class ParentBuilder < Visitor
4
+
5
+ def build node
6
+ link_parents node, nil
7
+ node
8
+ end
9
+
10
+ ## define methods for traverse different kinds of nodes
11
+ ## consts are from RKelly::Visitors::Visitor
12
+
13
+ TERMINAL_NODES.each do |type|
14
+ define_method(:"visit_#{type}Node") { |o| o.value }
15
+ end
16
+
17
+ BINARY_NODES.each do |type|
18
+ define_method(:"visit_#{type}Node") do |o|
19
+ [o.left && link_parents(o.left, o), o.value && link_parents(o.value, o)]
20
+ end
21
+ end
22
+
23
+ ARRAY_VALUE_NODES.each do |type|
24
+ define_method(:"visit_#{type}Node") do |o|
25
+ o.value && o.value.map { |v| v ? link_parents(v, o) : nil }
26
+ end
27
+ end
28
+
29
+ NAME_VALUE_NODES.each do |type|
30
+ define_method(:"visit_#{type}Node") do |o|
31
+ [o.name.to_s.to_sym, o.value ? link_parents(o.value, o) : nil]
32
+ end
33
+ end
34
+
35
+ SINGLE_VALUE_NODES.each do |type|
36
+ define_method(:"visit_#{type}Node") do |o|
37
+ link_parents(o.value, o) if o.value
38
+ end
39
+ end
40
+
41
+ PREFIX_POSTFIX_NODES.each do |type|
42
+ define_method(:"visit_#{type}Node") do |o|
43
+ link_parents(o.operand, o)
44
+ end
45
+
46
+ end
47
+
48
+ CONDITIONAL_NODES.each do |type|
49
+ define_method(:"visit_#{type}Node") do |o|
50
+ [ link_parents(o.conditions, o),
51
+ link_parents(o.value, o),
52
+ o.else ? link_parents(o.else, o) : nil
53
+ ]
54
+ end
55
+ end
56
+ FUNC_CALL_NODES.each do |type|
57
+ define_method(:"visit_#{type}Node") do |o|
58
+ [link_parents(o.value, o), link_parents(o.arguments, o)]
59
+ end
60
+ end
61
+ FUNC_DECL_NODES.each do |type|
62
+ define_method(:"visit_#{type}Node") do |o|
63
+ [
64
+ o.value ? o.value : nil,
65
+ o.arguments.map { |x| link_parents(x, o) },
66
+ link_parents(o.function_body, o)
67
+ ]
68
+ end
69
+ end
70
+
71
+ def visit_ForNode(o)
72
+ [
73
+ o.init ? link_parents(o.init, o) : nil,
74
+ o.test ? link_parents(o.test, o) : nil,
75
+ o.counter ? link_parents(o.counter, o) : nil,
76
+ link_parents(o.value, o)
77
+ ]
78
+ end
79
+
80
+ def visit_ForInNode(o)
81
+ [
82
+ link_parents(o.left, o),
83
+ link_parents(o.right, o),
84
+ link_parents(o.value, o)
85
+ ]
86
+ end
87
+
88
+ def visit_TryNode(o)
89
+ [
90
+ link_parents(o.value, o),
91
+ o.catch_var ? o.catch_var : nil,
92
+ o.catch_block ? link_parents(o.catch_block, o) : nil,
93
+ o.finally_block ? link_parents(o.finally_block, o) : nil
94
+ ]
95
+ end
96
+
97
+ def visit_BracketAccessorNode(o)
98
+ [
99
+ link_parents(o.value, o),
100
+ link_parents(o.accessor, o)
101
+ ]
102
+ end
103
+
104
+ def visit_DotAccessorNode(o)
105
+ link_parents(o.value, o)
106
+ end
107
+
108
+ private
109
+ def link_parents node, parent
110
+ node.parent = parent
111
+
112
+ # call the visit method to traverse potential subnodes depends on the node class
113
+ # error if the node is not in the predefined list
114
+ #
115
+ raise "No visit method for '#{node.class}'" unless self.respond_to?("visit_#{node.class.name.split(/::/)[-1]}")
116
+ self.send(:"visit_#{node.class.name.split(/::/)[-1]}", node)
117
+ end
118
+ end
119
+ end
120
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsinstrument
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mushan Yang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rkelly
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Instrument JavaScript source code to monitor code coverage in pure Ruby
63
+ email:
64
+ - mushan.yang@hulu.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - jsinstrument.gemspec
75
+ - lib/jsinstrument.rb
76
+ - lib/jsinstrument/inserting.js
77
+ - lib/jsinstrument/instrumenter.rb
78
+ - lib/jsinstrument/version.rb
79
+ - lib/rkelly/node.rb
80
+ - lib/rkelly/parent_builder.rb
81
+ homepage: ''
82
+ licenses:
83
+ - MIT
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 1.8.25
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: Instrument JavaScript source code to monitor code coverage in pure Ruby
106
+ test_files: []