vigetlabs-garb 0.1.2

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.
@@ -0,0 +1,211 @@
1
+ require 'ostruct'
2
+
3
+ module Garb
4
+ class Report
5
+ MONTH = 2592000
6
+ URL = "https://www.google.com/analytics/feeds/data"
7
+
8
+ METRICS = {
9
+ :visitor => [
10
+ :avgPageviews,
11
+ :avgSessionTime,
12
+ :bounces,
13
+ :bounceRate,
14
+ :entrances,
15
+ :exits,
16
+ :exitRate,
17
+ :newVisitors,
18
+ :pageDuration,
19
+ :pageviews,
20
+ :visitDuration,
21
+ :visitors,
22
+ :visits
23
+ ],
24
+ :campaign => [
25
+ :cost,
26
+ :clicks,
27
+ :clickThroughRate,
28
+ :costPerConversion,
29
+ :costPerGoalConversion,
30
+ :costPerMilleImpressions,
31
+ :costPerTransaction,
32
+ :impressions
33
+ ],
34
+ :content => [
35
+ :uniquePageviews
36
+ ],
37
+ :ecommerce => [
38
+ :productPurchases,
39
+ :productRevenue,
40
+ :products,
41
+ :revenue,
42
+ :revenuePerClick,
43
+ :revenuePerTransaction,
44
+ :revenuePerVisit,
45
+ :shipping,
46
+ :tax,
47
+ :transactions
48
+ ],
49
+ :internal_search => [
50
+ :searchDepth,
51
+ :searchDuration,
52
+ :searchExits,
53
+ :searchTransitions,
54
+ :uniqueInternalSearches,
55
+ :visitsWithSearches
56
+ ],
57
+ :goals => [
58
+ :goalCompletions1,
59
+ :goalCompletions2,
60
+ :goalCompletions3,
61
+ :goalCompletions4,
62
+ :goalCompletionsAll,
63
+ :goalConversionRate,
64
+ :goalStarts1,
65
+ :goalStarts2,
66
+ :goalStarts3,
67
+ :goalStarts4,
68
+ :goalStartsAll,
69
+ :goalValue1,
70
+ :goalValue2,
71
+ :goalValue3,
72
+ :goalValue4,
73
+ :goalValueAll,
74
+ :goalValuePerVisit
75
+ ]
76
+ }
77
+
78
+ DIMENSIONS = {
79
+ :visitor => [
80
+ :browser,
81
+ :browserVersion,
82
+ :city,
83
+ :connectionSpeed,
84
+ :continent,
85
+ :country,
86
+ :daysSinceLastVisit,
87
+ :domain,
88
+ :flashVersion,
89
+ :hostname,
90
+ :hour,
91
+ :javaEnabled,
92
+ :languqage,
93
+ :medium,
94
+ :organization,
95
+ :pageDepth,
96
+ :platform,
97
+ :platformVersion,
98
+ :referralPath,
99
+ :region,
100
+ :screenColors,
101
+ :screenResolution,
102
+ :subContinentRegion,
103
+ :userDefinedValue,
104
+ :visitNumber,
105
+ :visitorType
106
+ ],
107
+ :campaign => [
108
+ :adGroup,
109
+ :adSlot,
110
+ :adSlotPosition,
111
+ :campaign,
112
+ :content,
113
+ :keyword,
114
+ :source,
115
+ :sourceMedium
116
+ ],
117
+ :content => [
118
+ :pageTitle,
119
+ :requestUri,
120
+ :requestUri1,
121
+ :requestUriLast
122
+ ],
123
+ :ecommerce => [
124
+ :affiliation,
125
+ :daysToTransaction,
126
+ :productCode,
127
+ :productName,
128
+ :productVariation,
129
+ :transactionId,
130
+ :visitsToTransaction
131
+ ],
132
+ :internal_search => [
133
+ :hasInternalSearch,
134
+ :internalSearchKeyword,
135
+ :internalSearchNext,
136
+ :internalSearchType
137
+ ]
138
+ }
139
+
140
+ attr_accessor :metrics, :dimensions, :sort, :filters,
141
+ :start_date, :max_results, :end_date
142
+
143
+ attr_reader :profile
144
+
145
+ # def self.element_id(property_name)
146
+ # property_name.is_a?(Operator) ? property_name.to_s : property_name.to_ga.lower_camelized
147
+ # end
148
+ #
149
+ # def self.property_value(entry, property_name)
150
+ # (entry/property_name).first.inner_text
151
+ # end
152
+ #
153
+ # def self.property_values(entry, property_names)
154
+ # hash = property_names.inject({}) do |hash, property_name|
155
+ # hash.merge({property_name => property_value(entry, property_name.to_s.lower_camelized)})
156
+ # end
157
+ # OpenStruct.new hash
158
+ # end
159
+ #
160
+ def self.format_time(t)
161
+ t.strftime('%Y-%m-%d')
162
+ end
163
+
164
+ def initialize(profile, opts={})
165
+ @profile = profile
166
+
167
+ @sort = ReportParameter.new(:sort) << opts.fetch(:sort, [])
168
+ @filters = ReportParameter.new(:filters) << opts.fetch(:filters, [])
169
+ @metrics = ReportParameter.new(:metrics) << opts.fetch(:metrics, [])
170
+ @dimensions = ReportParameter.new(:dimensions) << opts.fetch(:dimensions, [])
171
+
172
+ @start_date = opts.fetch(:start_date, Time.now - MONTH)
173
+ @end_date = opts.fetch(:end_date, Time.now)
174
+
175
+ yield self if block_given?
176
+ end
177
+
178
+ def page_params
179
+ max_results.nil? ? {} : {'max-results' => max_results}
180
+ end
181
+
182
+ def default_params
183
+ {'ids' => profile.table_id,
184
+ 'start-date' => self.class.format_time(start_date),
185
+ 'end-date' => self.class.format_time(end_date)}
186
+ end
187
+
188
+ def params
189
+ [
190
+ metrics.to_params,
191
+ dimensions.to_params,
192
+ sort.to_params,
193
+ filters.to_params,
194
+ page_params
195
+ ].inject(default_params) do |p, i|
196
+ p.merge(i)
197
+ end
198
+ end
199
+
200
+ def send_request_for_body
201
+ request = DataRequest.new(URL, params)
202
+ response = request.send_request
203
+ response.body
204
+ end
205
+
206
+ def all
207
+ @entries = ReportResponse.new(send_request_for_body).parse
208
+ end
209
+
210
+ end
211
+ end
@@ -0,0 +1,36 @@
1
+ module Garb
2
+ class ReportParameter
3
+
4
+ attr_reader :elements
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @elements = []
9
+ end
10
+
11
+ def name
12
+ @name.to_s
13
+ end
14
+
15
+ def <<(element)
16
+ @elements += [element].flatten
17
+ self
18
+ end
19
+
20
+ def to_params
21
+ params = self.elements.map do |elem|
22
+ case elem
23
+ when Hash
24
+ elem.collect do |k,v|
25
+ next unless k.is_a?(Operator)
26
+ "#{k.target}#{CGI::escape(k.operator.to_s)}#{CGI::escape(v.to_s)}"
27
+ end.join(';')
28
+ else
29
+ elem.to_ga
30
+ end
31
+ end.join(',')
32
+
33
+ params.empty? ? {} : {self.name => params}
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,56 @@
1
+ module Garb
2
+ class ReportResponse
3
+ def initialize(response_body)
4
+ @xml = response_body
5
+ end
6
+
7
+ def parse
8
+ entries = Entry.parse(@xml)
9
+
10
+ entries.collect do |entry|
11
+ hash = {}
12
+
13
+ entry.metrics.each do |m|
14
+ name = m.name.sub(/^ga\:/,'').underscored
15
+ hash.merge!({name => m.value})
16
+ end
17
+
18
+ entry.dimensions.each do |d|
19
+ name = d.name.sub(/^ga\:/,'').underscored
20
+ hash.merge!({name => d.value})
21
+ end
22
+
23
+ OpenStruct.new(hash)
24
+ end
25
+ end
26
+
27
+ class Metric
28
+ include HappyMapper
29
+
30
+ tag 'metric'
31
+ namespace 'dxp'
32
+
33
+ attribute :name, String
34
+ attribute :value, String
35
+ end
36
+
37
+ class Dimension
38
+ include HappyMapper
39
+
40
+ tag 'dimension'
41
+ namespace 'dxp'
42
+
43
+ attribute :name, String
44
+ attribute :value, String
45
+ end
46
+
47
+ class Entry
48
+ include HappyMapper
49
+
50
+ tag 'entry'
51
+
52
+ has_many :metrics, Metric
53
+ has_many :dimensions, Dimension
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ module Garb
2
+ class Session
3
+
4
+ def self.login(email, password)
5
+ @email = email
6
+ auth_request = AuthenticationRequest.new(email, password)
7
+ @auth_token = auth_request.auth_token
8
+ end
9
+
10
+ def self.auth_token
11
+ @auth_token
12
+ end
13
+
14
+ def self.email
15
+ @email
16
+ end
17
+
18
+ end
19
+ end
data/lib/garb.rb ADDED
@@ -0,0 +1,67 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'rubygems'
6
+ require 'cgi'
7
+ require 'happymapper'
8
+
9
+ require 'garb/authentication_request'
10
+ require 'garb/data_request'
11
+ require 'garb/session'
12
+ require 'garb/profile'
13
+ require 'garb/report'
14
+ require 'garb/report_parameter'
15
+ require 'garb/report_response'
16
+
17
+ require 'extensions/string'
18
+ require 'extensions/operator'
19
+ require 'extensions/symbol'
20
+ require 'extensions/happymapper'
21
+
22
+ module Garb
23
+ # :stopdoc:
24
+ GA = "http://schemas.google.com/analytics/2008"
25
+
26
+ VERSION = '0.1.2'
27
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
28
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
29
+ # :startdoc:
30
+
31
+ # Returns the version string for the library.
32
+ #
33
+ def self.version
34
+ VERSION
35
+ end
36
+
37
+ # Returns the library path for the module. If any arguments are given,
38
+ # they will be joined to the end of the libray path using
39
+ # <tt>File.join</tt>.
40
+ #
41
+ def self.libpath( *args )
42
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
43
+ end
44
+
45
+ # Returns the lpath for the module. If any arguments are given,
46
+ # they will be joined to the end of the path using
47
+ # <tt>File.join</tt>.
48
+ #
49
+ def self.path( *args )
50
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
51
+ end
52
+
53
+ # Utility method used to rquire all files ending in .rb that lie in the
54
+ # directory below this file that has the same name as the filename passed
55
+ # in. Optionally, a specific _directory_ name can be passed in such that
56
+ # the _filename_ does not have to be equivalent to the directory.
57
+ #
58
+ def self.require_all_libs_relative_to( fname, dir = nil )
59
+ dir ||= ::File.basename(fname, '.*')
60
+ search_me = ::File.expand_path(
61
+ ::File.join(::File.dirname(fname), dir, '*', '*.rb'))
62
+
63
+ Dir.glob(search_me).sort.each {|rb| require rb}
64
+ end
65
+ end # module Garb
66
+
67
+ # EOF
@@ -0,0 +1,91 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module Garb
4
+ class AuthenticationRequestTest < Test::Unit::TestCase
5
+
6
+ context "An instance of the AuthenticationRequest class" do
7
+
8
+ setup { @request = AuthenticationRequest.new('email', 'password') }
9
+
10
+ should "have a collection of parameters that include the email and password" do
11
+ expected =
12
+ {
13
+ 'Email' => 'user@example.com',
14
+ 'Passwd' => 'fuzzybunnies',
15
+ 'accountType' => 'HOSTED_OR_GOOGLE',
16
+ 'service' => 'analytics',
17
+ 'source' => 'vigetLabs-garb-001'
18
+ }
19
+
20
+ request = AuthenticationRequest.new('user@example.com', 'fuzzybunnies')
21
+ assert_equal expected, request.parameters
22
+ end
23
+
24
+ should "have a URI" do
25
+ assert_equal URI.parse('https://www.google.com/accounts/ClientLogin'), @request.uri
26
+ end
27
+
28
+
29
+ should "be able to send a request to the GAAPI service" do
30
+ @request.expects(:build_request).returns('post')
31
+ response = mock {|m| m.expects(:is_a?).with(Net::HTTPOK).returns(true) }
32
+
33
+ http = mock do |m|
34
+ m.expects(:use_ssl=).with(true)
35
+ m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
36
+ m.expects(:request).with('post').yields(response)
37
+ end
38
+
39
+ Net::HTTP.expects(:new).with('www.google.com', 443).returns(http)
40
+
41
+ @request.send_request
42
+ end
43
+
44
+ should "be able to build a request for the GAAPI service" do
45
+ params = "param"
46
+ @request.expects(:parameters).with().returns(params)
47
+
48
+ post = mock
49
+ post.expects(:set_form_data).with(params)
50
+
51
+ Net::HTTP::Post.expects(:new).with('/accounts/ClientLogin').returns(post)
52
+
53
+ @request.build_request
54
+ end
55
+
56
+ should "be able to retrieve an auth_token from the body" do
57
+ response_data =
58
+ "SID=mysid\n" +
59
+ "LSID=mylsid\n" +
60
+ "Auth=auth_token\n"
61
+
62
+ @request.expects(:send_request).with().returns(stub(:body => response_data))
63
+
64
+ assert_equal 'auth_token', @request.auth_token
65
+ end
66
+
67
+ should "raise an exception when requesting an auth_token when the authorization fails" do
68
+ @request.stubs(:build_request)
69
+ response = mock do |m|
70
+ m.expects(:is_a?).with(Net::HTTPOK).returns(false)
71
+ end
72
+
73
+ http = stub do |s|
74
+ s.stubs(:use_ssl=)
75
+ s.stubs(:verify_mode=)
76
+ s.stubs(:request).yields(response)
77
+ end
78
+
79
+ Net::HTTP.stubs(:new).with('www.google.com', 443).returns(http)
80
+
81
+ assert_raise(Garb::AuthenticationRequest::AuthError) do
82
+ @request.send_request
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+
89
+
90
+ end
91
+ end
@@ -0,0 +1,52 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ module Garb
4
+ class DataRequestTest < Test::Unit::TestCase
5
+
6
+ context "An instance of the DataRequest class" do
7
+
8
+ should "be able to build the query string from parameters" do
9
+ parameters = {'ids' => '12345', 'metrics' => 'country'}
10
+ data_request = DataRequest.new("", parameters)
11
+
12
+ query_string = data_request.query_string
13
+
14
+ assert_match(/^\?/, query_string)
15
+
16
+ query_string.sub!(/^\?/, '')
17
+
18
+ assert_equal ["ids=12345", "metrics=country"], query_string.split('&').sort
19
+ end
20
+
21
+ should "return an empty query string if parameters are empty" do
22
+ data_request = DataRequest.new("")
23
+ assert_equal "", data_request.query_string
24
+ end
25
+
26
+ should "be able to build a uri" do
27
+ url = 'http://example.com'
28
+ expected = URI.parse('http://example.com')
29
+
30
+ assert_equal expected, DataRequest.new(url).uri
31
+ end
32
+
33
+ should "be able to make a request to the GAAPI" do
34
+ Session.expects(:auth_token).with().returns('toke')
35
+ response = mock
36
+ response.expects(:is_a?).with(Net::HTTPOK).returns(true)
37
+
38
+ http = mock do |m|
39
+ m.expects(:use_ssl=).with(true)
40
+ m.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
41
+ m.expects(:get).with('/data?key=value', 'Authorization' => 'GoogleLogin auth=toke').returns(response)
42
+ end
43
+
44
+ Net::HTTP.expects(:new).with('example.com', 443).returns(http)
45
+
46
+ data_request = DataRequest.new('https://example.com/data', 'key' => 'value')
47
+ assert_equal response, data_request.send_request
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,33 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:dxp='http://schemas.google.com/analytics/2009'>
3
+ <id>http://www.google.com/analytics/feeds/accounts/email@example.com</id>
4
+ <updated>2009-03-27T08:14:28.000-07:00</updated>
5
+ <title type='text'>Profile list for email@example.com</title>
6
+ <link rel='self' type='application/atom+xml' href='http://www.google.com/analytics/feeds/accounts/email@example.com'/>
7
+ <author><name>Google Analytics</name></author>
8
+ <generator version='1.0'>Google Analytics</generator>
9
+ <openSearch:totalResults>2</openSearch:totalResults>
10
+ <openSearch:startIndex>1</openSearch:startIndex>
11
+ <openSearch:itemsPerPage>2</openSearch:itemsPerPage>
12
+ <entry>
13
+ <id>http://www.google.com/analytics/feeds/accounts/ga:12345</id>
14
+ <updated>2008-07-21T14:05:57.000-07:00</updated>
15
+ <title type='text'>Historical</title>
16
+ <dxp:tableId>ga:12345</dxp:tableId>
17
+ <dxp:property name='ga:accountId' value='1111'/>
18
+ <dxp:property name='ga:accountName' value='Blog Beta'/>
19
+ <dxp:property name='ga:profileId' value='1212'/>
20
+ <dxp:property name='ga:webPropertyId' value='UA-1111-1'/>
21
+ </entry>
22
+ <entry>
23
+ <id>http://www.google.com/analytics/feeds/accounts/ga:12346</id>
24
+ <updated>2008-11-24T11:51:07.000-08:00</updated>
25
+ <title type='text'>Presently</title>
26
+ <dxp:tableId>ga:12346</dxp:tableId>
27
+ <dxp:property name='ga:accountId' value='1111'/>
28
+ <dxp:property name='ga:accountName' value='Blog Beta'/>
29
+ <dxp:property name='ga:profileId' value='1213'/>
30
+ <dxp:property name='ga:webPropertyId' value='UA-1111-2'/>
31
+ </entry>
32
+ </feed>
33
+
@@ -0,0 +1,46 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <feed xmlns='http://www.w3.org/2005/Atom'
3
+ xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
4
+ xmlns:dxp='http://schemas.google.com/dataexport/2009'
5
+ xmlns:ga='http://schemas.google.com/analytics/2008'>
6
+ <id>http://www.google.com/analytics/feeds/data?ids=ga:983247&amp;dimensions=ga:country,ga:city&amp;metrics=ga:pageViews&amp;start-date=2008-01-01&amp;end-date=2008-01-02</id>
7
+ <updated>2008-01-02T15:59:59.999-08:00 </updated>
8
+ <title type="text">Google Analytics Data for Profile 983247</title>
9
+ <link href="http://www.google.com/analytics/feeds/data" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
10
+ <link href="http://www.google.com/analytics/feeds/data?end-date=2008-01-02&amp;start-date=2008-01-01&amp;metrics=ga%3ApageViews&amp;ids=ga%3A983247&amp;dimensions=ga%3Acountry%2Cga%3Acity" rel="self" type="application/atom+xml"/>
11
+ <link href="http://www.google.com/analytics/feeds/data?start-index=1001&amp;max-results=1000&amp;end-date=2008-01-02&amp;start-date=2008-01-01&amp;metrics=ga%3ApageViews&amp;ids=ga%3A983247&amp;dimensions=ga%3Acountry%2Cga%3Acity" rel="next" type="application/atom+xml"/>
12
+ <author>
13
+ <name>Google Analytics</name>
14
+ </author>
15
+ <openSearch:startIndex>3</openSearch:startIndex>
16
+ <openSearch:itemsPerPage>4</openSearch:itemsPerPage>
17
+ <ga:webPropertyID>UA-983247-67</ga:webPropertyID>
18
+ <ga:start-date>2008-01-01</ga:start-date>
19
+ <ga:end-date>2008-01-02</ga:end-date>
20
+
21
+ <entry>
22
+ <id> http://www.google.com/analytics/feeds/data?ids=ga:1174&amp;ga:country=%28not%20set%29&amp;ga:city=%28not%20set%29&amp;start-date=2008-01-01&amp;end-date=2008-01-02 </id>
23
+ <updated> 2008-01-01T16:00:00.001-08:00 </updated>
24
+ <title type="text"> ga:country=(not set) | ga:city=(not set) </title>
25
+ <link href="http://www.google.com/analytics/feeds/data" rel="self" type="application/atom+xml"/>
26
+ <dxp:dimension name="ga:country" value="(not set)" />
27
+ <dxp:dimension name="ga:city" value="(not set)" />
28
+ <dxp:metric name="ga:pageviews" value="33" />
29
+ </entry>
30
+ <entry>
31
+ <id> http://www.google.com/analytics/feeds/data?ids=ga:1174&amp;ga:country=Afghanistan&amp;ga:city=Kabul&amp;start-date=2008-01-01&amp;end-date=2008-01-02 </id>
32
+ <updated> 2008-01-01T16:00:00.001-08:00 </updated>
33
+ <title type="text"> ga:country=Afghanistan | ga:city=Kabul </title>
34
+ <dxp:dimension name="ga:country" value="Afghanistan" />
35
+ <dxp:dimension name="ga:city" value="Kabul" />
36
+ <dxp:metric name="ga:pageviews" value="2" />
37
+ </entry>
38
+ <entry>
39
+ <id> http://www.google.com/analytics/feeds/data?ids=ga:1174&amp;ga:country=Albania&amp;ga:city=Tirana&amp;start-date=2008-01-01&amp;end-date=2008-01-02 </id>
40
+ <updated> 2008-01-01T16:00:00.001-08:00 </updated>
41
+ <title type="text"> ga:country=Albania | ga:city=Tirana </title>
42
+ <dxp:dimension name="ga:country" value="Albania" />
43
+ <dxp:dimension name="ga:city" value="Tirana" />
44
+ <dxp:metric name="ga:pageviews" value="1" />
45
+ </entry>
46
+ </feed>
data/test/garb_test.rb ADDED
@@ -0,0 +1,9 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class GarbTest < Test::Unit::TestCase
4
+ context "A green egg" do
5
+ should "be served with ham" do
6
+ assert true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class OperatorTest < Test::Unit::TestCase
4
+ context "An instance of an Operator" do
5
+ should "lower camelize the target" do
6
+ assert_equal "ga:uniqueVisits=", Operator.new(:unique_visits, "=").to_ga
7
+ end
8
+
9
+ should "return target and operator together" do
10
+ assert_equal "ga:metric=", Operator.new(:metric, "=").to_ga
11
+ end
12
+
13
+ should "prefix the operator to the target" do
14
+ assert_equal "-ga:metric", Operator.new(:metric, "-", true).to_ga
15
+ end
16
+
17
+ should "know if it is equal to another operator" do
18
+ op1 = Operator.new(:hello, "==")
19
+ op2 = Operator.new(:hello, "==")
20
+ assert_equal op1, op2
21
+ end
22
+
23
+ should "not be equal to another operator if target, operator, or prefix is different" do
24
+ op1 = Operator.new(:hello, "==")
25
+ op2 = Operator.new(:hello, "==", true)
26
+ assert_not_equal op1, op2
27
+
28
+ op1 = Operator.new(:hello1, "==")
29
+ op2 = Operator.new(:hello2, "==")
30
+ assert_not_equal op1, op2
31
+
32
+ op1 = Operator.new(:hello, "!=")
33
+ op2 = Operator.new(:hello, "==")
34
+ assert_not_equal op1, op2
35
+ end
36
+ end
37
+ end