sauce 0.12.9 → 0.12.10
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/VERSION +1 -1
 - data/support/sauce_connect +48 -37
 - metadata +4 -4
 
    
        data/VERSION
    CHANGED
    
    | 
         @@ -1 +1 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            0.12. 
     | 
| 
      
 1 
     | 
    
         
            +
            0.12.10
         
     | 
    
        data/support/sauce_connect
    CHANGED
    
    | 
         @@ -27,6 +27,7 @@ import time 
     | 
|
| 
       27 
27 
     | 
    
         
             
            import platform
         
     | 
| 
       28 
28 
     | 
    
         
             
            import tempfile
         
     | 
| 
       29 
29 
     | 
    
         
             
            import string
         
     | 
| 
      
 30 
     | 
    
         
            +
            from base64 import b64encode
         
     | 
| 
       30 
31 
     | 
    
         
             
            from collections import defaultdict
         
     | 
| 
       31 
32 
     | 
    
         
             
            from contextlib import closing
         
     | 
| 
       32 
33 
     | 
    
         
             
            from functools import wraps
         
     | 
| 
         @@ -37,7 +38,7 @@ except ImportError: 
     | 
|
| 
       37 
38 
     | 
    
         
             
                import simplejson as json  # Python 2.5 dependency
         
     | 
| 
       38 
39 
     | 
    
         | 
| 
       39 
40 
     | 
    
         
             
            NAME = "sauce_connect"
         
     | 
| 
       40 
     | 
    
         
            -
            RELEASE =  
     | 
| 
      
 41 
     | 
    
         
            +
            RELEASE = 25
         
     | 
| 
       41 
42 
     | 
    
         
             
            DISPLAY_VERSION = "%s release %s" % (NAME, RELEASE)
         
     | 
| 
       42 
43 
     | 
    
         
             
            PRODUCT_NAME = u"Sauce Connect"
         
     | 
| 
       43 
44 
     | 
    
         
             
            VERSIONS_URL = "http://saucelabs.com/versions.json"
         
     | 
| 
         @@ -59,6 +60,12 @@ is_openbsd = platform.system().lower() == "openbsd" 
     | 
|
| 
       59 
60 
     | 
    
         
             
            logger = logging.getLogger(NAME)
         
     | 
| 
       60 
61 
     | 
    
         | 
| 
       61 
62 
     | 
    
         | 
| 
      
 63 
     | 
    
         
            +
            class DeleteRequest(urllib2.Request):
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                def get_method(self):
         
     | 
| 
      
 66 
     | 
    
         
            +
                    return "DELETE"
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
       62 
69 
     | 
    
         
             
            class HTTPResponseError(Exception):
         
     | 
| 
       63 
70 
     | 
    
         | 
| 
       64 
71 
     | 
    
         
             
                def __init__(self, msg):
         
     | 
| 
         @@ -84,18 +91,19 @@ class TunnelMachine(object): 
     | 
|
| 
       84 
91 
     | 
    
         | 
| 
       85 
92 
     | 
    
         
             
                _host_search = re.compile("//([^/]+)").search
         
     | 
| 
       86 
93 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                def __init__(self, rest_url, user, password, domains, metadata=None):
         
     | 
| 
      
 94 
     | 
    
         
            +
                def __init__(self, rest_url, user, password, domains, ssh_port, metadata=None):
         
     | 
| 
       88 
95 
     | 
    
         
             
                    self.user = user
         
     | 
| 
       89 
96 
     | 
    
         
             
                    self.password = password
         
     | 
| 
       90 
97 
     | 
    
         
             
                    self.domains = set(domains)
         
     | 
| 
      
 98 
     | 
    
         
            +
                    self.ssh_port = ssh_port
         
     | 
| 
       91 
99 
     | 
    
         
             
                    self.metadata = metadata or dict()
         
     | 
| 
       92 
100 
     | 
    
         | 
| 
       93 
101 
     | 
    
         
             
                    self.reverse_ssh = None
         
     | 
| 
       94 
102 
     | 
    
         
             
                    self.is_shutdown = False
         
     | 
| 
       95 
103 
     | 
    
         
             
                    self.base_url = "%(rest_url)s/%(user)s/tunnels" % locals()
         
     | 
| 
       96 
104 
     | 
    
         
             
                    self.rest_host = self._host_search(rest_url).group(1)
         
     | 
| 
       97 
     | 
    
         
            -
                    self.basic_auth_header = {"Authorization": "Basic %s" 
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
      
 105 
     | 
    
         
            +
                    self.basic_auth_header = {"Authorization": "Basic %s"
         
     | 
| 
      
 106 
     | 
    
         
            +
                                              % b64encode("%s:%s" % (user, password))}
         
     | 
| 
       99 
107 
     | 
    
         | 
| 
       100 
108 
     | 
    
         
             
                    self._set_urlopen(user, password)
         
     | 
| 
       101 
109 
     | 
    
         | 
| 
         @@ -111,9 +119,7 @@ class TunnelMachine(object): 
     | 
|
| 
       111 
119 
     | 
    
         
             
                                    "help@saucelabs.com.")
         
     | 
| 
       112 
120 
     | 
    
         | 
| 
       113 
121 
     | 
    
         
             
                def _set_urlopen(self, user, password):
         
     | 
| 
       114 
     | 
    
         
            -
                    # always send Basic Auth header  
     | 
| 
       115 
     | 
    
         
            -
                    # NOTE: we directly construct the header because it is more reliable
         
     | 
| 
       116 
     | 
    
         
            -
                    #   and more efficient than HTTPBasicAuthHandler and we always need it
         
     | 
| 
      
 122 
     | 
    
         
            +
                    # always send Basic Auth header (HTTPBasicAuthHandler was unreliable)
         
     | 
| 
       117 
123 
     | 
    
         
             
                    opener = urllib2.build_opener()
         
     | 
| 
       118 
124 
     | 
    
         
             
                    opener.addheaders = self.basic_auth_header.items()
         
     | 
| 
       119 
125 
     | 
    
         
             
                    self.urlopen = opener.open
         
     | 
| 
         @@ -155,21 +161,6 @@ class TunnelMachine(object): 
     | 
|
| 
       155 
161 
     | 
    
         
             
                            raise HTTPResponseError(resp.msg)
         
     | 
| 
       156 
162 
     | 
    
         
             
                        return json.loads(resp.read())
         
     | 
| 
       157 
163 
     | 
    
         | 
| 
       158 
     | 
    
         
            -
                @_retry_rest_api
         
     | 
| 
       159 
     | 
    
         
            -
                def _get_delete_doc(self, url):
         
     | 
| 
       160 
     | 
    
         
            -
                    # urllib2 doesn support the DELETE method (lame), so we build our own
         
     | 
| 
       161 
     | 
    
         
            -
                    if self.base_url.startswith("https"):
         
     | 
| 
       162 
     | 
    
         
            -
                        make_conn = httplib.HTTPSConnection
         
     | 
| 
       163 
     | 
    
         
            -
                    else:
         
     | 
| 
       164 
     | 
    
         
            -
                        make_conn = httplib.HTTPConnection
         
     | 
| 
       165 
     | 
    
         
            -
                    with closing(make_conn(self.rest_host)) as conn:
         
     | 
| 
       166 
     | 
    
         
            -
                        conn.request(method="DELETE", url=url,
         
     | 
| 
       167 
     | 
    
         
            -
                                     headers=self.basic_auth_header)
         
     | 
| 
       168 
     | 
    
         
            -
                        resp = conn.getresponse()
         
     | 
| 
       169 
     | 
    
         
            -
                        if resp.reason != "OK":
         
     | 
| 
       170 
     | 
    
         
            -
                            raise HTTPResponseError(resp.reason)
         
     | 
| 
       171 
     | 
    
         
            -
                        return json.loads(resp.read())
         
     | 
| 
       172 
     | 
    
         
            -
             
     | 
| 
       173 
164 
     | 
    
         
             
                def _provision_tunnel(self):
         
     | 
| 
       174 
165 
     | 
    
         
             
                    # Shutdown any tunnel using a requested domain
         
     | 
| 
       175 
166 
     | 
    
         
             
                    kill_list = set()
         
     | 
| 
         @@ -186,7 +177,7 @@ class TunnelMachine(object): 
     | 
|
| 
       186 
177 
     | 
    
         
             
                                logger.debug(
         
     | 
| 
       187 
178 
     | 
    
         
             
                                    "Shutting down old tunnel host: %s" % tunnel_id)
         
     | 
| 
       188 
179 
     | 
    
         
             
                                url = "%s/%s" % (self.base_url, tunnel_id)
         
     | 
| 
       189 
     | 
    
         
            -
                                doc = self. 
     | 
| 
      
 180 
     | 
    
         
            +
                                doc = self._get_doc(DeleteRequest(url=url))
         
     | 
| 
       190 
181 
     | 
    
         
             
                                if not doc.get('ok'):
         
     | 
| 
       191 
182 
     | 
    
         
             
                                    logger.warning("Old tunnel host failed to shutdown?")
         
     | 
| 
       192 
183 
     | 
    
         
             
                                    continue
         
     | 
| 
         @@ -201,7 +192,8 @@ class TunnelMachine(object): 
     | 
|
| 
       201 
192 
     | 
    
         
             
                    # Request a tunnel machine
         
     | 
| 
       202 
193 
     | 
    
         
             
                    headers = {"Content-Type": "application/json"}
         
     | 
| 
       203 
194 
     | 
    
         
             
                    data = json.dumps(dict(DomainNames=list(self.domains),
         
     | 
| 
       204 
     | 
    
         
            -
                                           Metadata=self.metadata 
     | 
| 
      
 195 
     | 
    
         
            +
                                           Metadata=self.metadata,
         
     | 
| 
      
 196 
     | 
    
         
            +
                                           SSHPort=self.ssh_port))
         
     | 
| 
       205 
197 
     | 
    
         
             
                    req = urllib2.Request(url=self.base_url, headers=headers, data=data)
         
     | 
| 
       206 
198 
     | 
    
         
             
                    doc = self._get_doc(req)
         
     | 
| 
       207 
199 
     | 
    
         
             
                    if doc.get('error'):
         
     | 
| 
         @@ -243,9 +235,10 @@ class TunnelMachine(object): 
     | 
|
| 
       243 
235 
     | 
    
         
             
                    logger.debug("Tunnel host ID: %s" % self.id)
         
     | 
| 
       244 
236 
     | 
    
         | 
| 
       245 
237 
     | 
    
         
             
                    try:
         
     | 
| 
       246 
     | 
    
         
            -
                        doc = self. 
     | 
| 
       247 
     | 
    
         
            -
                    except TunnelMachineError:
         
     | 
| 
      
 238 
     | 
    
         
            +
                        doc = self._get_doc(DeleteRequest(url=self.url))
         
     | 
| 
      
 239 
     | 
    
         
            +
                    except TunnelMachineError, e:
         
     | 
| 
       248 
240 
     | 
    
         
             
                        logger.warning("Unable to shut down tunnel host")
         
     | 
| 
      
 241 
     | 
    
         
            +
                        logger.debug("Shut down failed because: %s", str(e))
         
     | 
| 
       249 
242 
     | 
    
         
             
                        self.is_shutdown = True  # fuhgeddaboudit
         
     | 
| 
       250 
243 
     | 
    
         
             
                        return
         
     | 
| 
       251 
244 
     | 
    
         
             
                    assert doc.get('ok')
         
     | 
| 
         @@ -350,13 +343,14 @@ class ReverseSSHError(Exception): 
     | 
|
| 
       350 
343 
     | 
    
         | 
| 
       351 
344 
     | 
    
         
             
            class ReverseSSH(object):
         
     | 
| 
       352 
345 
     | 
    
         | 
| 
       353 
     | 
    
         
            -
                def __init__(self, tunnel, host, ports, tunnel_ports,
         
     | 
| 
      
 346 
     | 
    
         
            +
                def __init__(self, tunnel, host, ports, tunnel_ports, ssh_port,
         
     | 
| 
       354 
347 
     | 
    
         
             
                             use_ssh_config=False, debug=False):
         
     | 
| 
       355 
348 
     | 
    
         
             
                    self.tunnel = tunnel
         
     | 
| 
       356 
349 
     | 
    
         
             
                    self.host = host
         
     | 
| 
       357 
350 
     | 
    
         
             
                    self.ports = ports
         
     | 
| 
       358 
351 
     | 
    
         
             
                    self.tunnel_ports = tunnel_ports
         
     | 
| 
       359 
352 
     | 
    
         
             
                    self.use_ssh_config = use_ssh_config
         
     | 
| 
      
 353 
     | 
    
         
            +
                    self.ssh_port = ssh_port
         
     | 
| 
       360 
354 
     | 
    
         
             
                    self.debug = debug
         
     | 
| 
       361 
355 
     | 
    
         | 
| 
       362 
356 
     | 
    
         
             
                    self.proc = None
         
     | 
| 
         @@ -388,8 +382,8 @@ class ReverseSSH(object): 
     | 
|
| 
       388 
382 
     | 
    
         
             
                def get_plink_command(self):
         
     | 
| 
       389 
383 
     | 
    
         
             
                    """Return the Windows SSH command."""
         
     | 
| 
       390 
384 
     | 
    
         
             
                    verbosity = "-v" if self.debug else ""
         
     | 
| 
       391 
     | 
    
         
            -
                    return ("plink\plink %s -l %s -pw %s -N %s %s"
         
     | 
| 
       392 
     | 
    
         
            -
                            % (verbosity, self.tunnel.user, self.tunnel.password,
         
     | 
| 
      
 385 
     | 
    
         
            +
                    return ("plink\plink %s -P %s -l %s -pw %s -N %s %s"
         
     | 
| 
      
 386 
     | 
    
         
            +
                            % (verbosity, self.ssh_port, self.tunnel.user, self.tunnel.password,
         
     | 
| 
       393 
387 
     | 
    
         
             
                               self._dash_Rs, self.tunnel.host))
         
     | 
| 
       394 
388 
     | 
    
         | 
| 
       395 
389 
     | 
    
         
             
                def get_expect_script(self):
         
     | 
| 
         @@ -402,8 +396,8 @@ class ReverseSSH(object): 
     | 
|
| 
       402 
396 
     | 
    
         
             
                    config_file = "" if self.use_ssh_config else "-F /dev/null"
         
     | 
| 
       403 
397 
     | 
    
         
             
                    host_ip = socket.gethostbyname(self.tunnel.host)
         
     | 
| 
       404 
398 
     | 
    
         
             
                    script = (
         
     | 
| 
       405 
     | 
    
         
            -
                        "spawn ssh %s %s -p  
     | 
| 
       406 
     | 
    
         
            -
                            % (verbosity, config_file, self.tunnel.user,
         
     | 
| 
      
 399 
     | 
    
         
            +
                        "spawn ssh %s %s -p %s -l %s -o ServerAliveInterval=%s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -N %s %s;"
         
     | 
| 
      
 400 
     | 
    
         
            +
                            % (verbosity, config_file, self.ssh_port, self.tunnel.user,
         
     | 
| 
       407 
401 
     | 
    
         
             
                               HEALTH_CHECK_INTERVAL, self._dash_Rs, self.tunnel.host) +
         
     | 
| 
       408 
402 
     | 
    
         
             
                        "expect *password:;send -- %s\\r;" % self.tunnel.password +
         
     | 
| 
       409 
403 
     | 
    
         
             
                        "expect -timeout -1 timeout")
         
     | 
| 
         @@ -433,9 +427,9 @@ class ReverseSSH(object): 
     | 
|
| 
       433 
427 
     | 
    
         | 
| 
       434 
428 
     | 
    
         
             
                    # setup recurring healthchecks
         
     | 
| 
       435 
429 
     | 
    
         
             
                    forwarded_health = HealthChecker(self.host, self.ports)
         
     | 
| 
       436 
     | 
    
         
            -
                    tunnel_health = HealthChecker(host=self.tunnel.host, ports=[ 
     | 
| 
      
 430 
     | 
    
         
            +
                    tunnel_health = HealthChecker(host=self.tunnel.host, ports=[self.ssh_port],
         
     | 
| 
       437 
431 
     | 
    
         
             
                        fail_msg="!! Your tests may fail because your network can not get "
         
     | 
| 
       438 
     | 
    
         
            -
                                 "to the tunnel host (%s:%d)." % (self.tunnel.host,  
     | 
| 
      
 432 
     | 
    
         
            +
                                 "to the tunnel host (%s:%d)." % (self.tunnel.host, self.ssh_port))
         
     | 
| 
       439 
433 
     | 
    
         | 
| 
       440 
434 
     | 
    
         
             
                    start_time = int(time.time())
         
     | 
| 
       441 
435 
     | 
    
         
             
                    while self.proc.poll() is None:
         
     | 
| 
         @@ -719,6 +713,8 @@ Performance tip: 
     | 
|
| 
       719 
713 
     | 
    
         
             
                              help=optparse.SUPPRESS_HELP)
         
     | 
| 
       720 
714 
     | 
    
         
             
                og.add_option("--allow-unclean-exit", action="store_true", default=False,
         
     | 
| 
       721 
715 
     | 
    
         
             
                              help=optparse.SUPPRESS_HELP)
         
     | 
| 
      
 716 
     | 
    
         
            +
                og.add_option("--ssh-port", default=22, type="int",
         
     | 
| 
      
 717 
     | 
    
         
            +
                              help=optparse.SUPPRESS_HELP)
         
     | 
| 
       722 
718 
     | 
    
         
             
                op.add_option_group(og)
         
     | 
| 
       723 
719 
     | 
    
         | 
| 
       724 
720 
     | 
    
         
             
                og = optparse.OptionGroup(op, "Script debugging options")
         
     | 
| 
         @@ -730,6 +726,20 @@ Performance tip: 
     | 
|
| 
       730 
726 
     | 
    
         | 
| 
       731 
727 
     | 
    
         
             
                (options, args) = op.parse_args()
         
     | 
| 
       732 
728 
     | 
    
         | 
| 
      
 729 
     | 
    
         
            +
                # check ports are numbers
         
     | 
| 
      
 730 
     | 
    
         
            +
                try:
         
     | 
| 
      
 731 
     | 
    
         
            +
                    map(int, options.ports)
         
     | 
| 
      
 732 
     | 
    
         
            +
                    map(int, options.tunnel_ports)
         
     | 
| 
      
 733 
     | 
    
         
            +
                except ValueError:
         
     | 
| 
      
 734 
     | 
    
         
            +
                    sys.stderr.write("Error: Ports must be integers\n\n")
         
     | 
| 
      
 735 
     | 
    
         
            +
                    print "Help with options -t and -p:"
         
     | 
| 
      
 736 
     | 
    
         
            +
                    print "  All ports must be integers. You used:"
         
     | 
| 
      
 737 
     | 
    
         
            +
                    if options.ports:
         
     | 
| 
      
 738 
     | 
    
         
            +
                        print "    -p", " -p ".join(options.ports)
         
     | 
| 
      
 739 
     | 
    
         
            +
                    if options.tunnel_ports:
         
     | 
| 
      
 740 
     | 
    
         
            +
                        print "    -t", " -t ".join(options.tunnel_ports)
         
     | 
| 
      
 741 
     | 
    
         
            +
                    raise SystemExit(1)
         
     | 
| 
      
 742 
     | 
    
         
            +
             
     | 
| 
       733 
743 
     | 
    
         
             
                # default to 80 and default to matching host ports with tunnel ports
         
     | 
| 
       734 
744 
     | 
    
         
             
                if not options.ports and not options.tunnel_ports:
         
     | 
| 
       735 
745 
     | 
    
         
             
                    options.ports = ["80"]
         
     | 
| 
         @@ -830,6 +840,7 @@ def run(options, dependency_versions=None): 
     | 
|
| 
       830 
840 
     | 
    
         
             
                    print "|  Contact us: http://saucelabs.com/forums          |"
         
     | 
| 
       831 
841 
     | 
    
         
             
                    print "-----------------------------------------------------"
         
     | 
| 
       832 
842 
     | 
    
         
             
                logger.info("/ Starting \\")
         
     | 
| 
      
 843 
     | 
    
         
            +
                logger.info('Please wait for "You may start your tests" to start your tests.')
         
     | 
| 
       833 
844 
     | 
    
         
             
                logger.info("%s" % DISPLAY_VERSION)
         
     | 
| 
       834 
845 
     | 
    
         
             
                check_version()
         
     | 
| 
       835 
846 
     | 
    
         | 
| 
         @@ -861,7 +872,8 @@ def run(options, dependency_versions=None): 
     | 
|
| 
       861 
872 
     | 
    
         
             
                for attempt in xrange(1, RETRY_BOOT_MAX + 1):
         
     | 
| 
       862 
873 
     | 
    
         
             
                    try:
         
     | 
| 
       863 
874 
     | 
    
         
             
                        tunnel = TunnelMachine(options.rest_url, options.user,
         
     | 
| 
       864 
     | 
    
         
            -
                                               options.api_key, options.domains, 
     | 
| 
      
 875 
     | 
    
         
            +
                                               options.api_key, options.domains,
         
     | 
| 
      
 876 
     | 
    
         
            +
                                               options.ssh_port, metadata)
         
     | 
| 
       865 
877 
     | 
    
         
             
                    except TunnelMachineError, e:
         
     | 
| 
       866 
878 
     | 
    
         
             
                        logger.error(e)
         
     | 
| 
       867 
879 
     | 
    
         
             
                        peace_out(returncode=1)  # exits
         
     | 
| 
         @@ -881,6 +893,7 @@ def run(options, dependency_versions=None): 
     | 
|
| 
       881 
893 
     | 
    
         | 
| 
       882 
894 
     | 
    
         
             
                ssh = ReverseSSH(tunnel=tunnel, host=options.host,
         
     | 
| 
       883 
895 
     | 
    
         
             
                                 ports=options.ports, tunnel_ports=options.tunnel_ports,
         
     | 
| 
      
 896 
     | 
    
         
            +
                                 ssh_port=options.ssh_port,
         
     | 
| 
       884 
897 
     | 
    
         
             
                                 use_ssh_config=options.use_ssh_config,
         
     | 
| 
       885 
898 
     | 
    
         
             
                                 debug=options.debug_ssh)
         
     | 
| 
       886 
899 
     | 
    
         
             
                try:
         
     | 
| 
         @@ -891,9 +904,7 @@ def run(options, dependency_versions=None): 
     | 
|
| 
       891 
904 
     | 
    
         | 
| 
       892 
905 
     | 
    
         | 
| 
       893 
906 
     | 
    
         
             
            def main():
         
     | 
| 
       894 
     | 
    
         
            -
                 
     | 
| 
       895 
     | 
    
         
            -
                pyver = float("%s.%s" % tuple(platform.python_version().split('.')[:2]))
         
     | 
| 
       896 
     | 
    
         
            -
                if pyver < 2.5:
         
     | 
| 
      
 907 
     | 
    
         
            +
                if map(int, platform.python_version_tuple ()) < [2, 5]:
         
     | 
| 
       897 
908 
     | 
    
         
             
                    print "%s requires Python 2.5 (2006) or newer." % PRODUCT_NAME
         
     | 
| 
       898 
909 
     | 
    
         
             
                    raise SystemExit(1)
         
     | 
| 
       899 
910 
     | 
    
         | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,13 +1,13 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification 
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: sauce
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version 
         
     | 
| 
       4 
     | 
    
         
            -
              hash:  
     | 
| 
      
 4 
     | 
    
         
            +
              hash: 59
         
     | 
| 
       5 
5 
     | 
    
         
             
              prerelease: 
         
     | 
| 
       6 
6 
     | 
    
         
             
              segments: 
         
     | 
| 
       7 
7 
     | 
    
         
             
              - 0
         
     | 
| 
       8 
8 
     | 
    
         
             
              - 12
         
     | 
| 
       9 
     | 
    
         
            -
              -  
     | 
| 
       10 
     | 
    
         
            -
              version: 0.12. 
     | 
| 
      
 9 
     | 
    
         
            +
              - 10
         
     | 
| 
      
 10 
     | 
    
         
            +
              version: 0.12.10
         
     | 
| 
       11 
11 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       12 
12 
     | 
    
         
             
            authors: 
         
     | 
| 
       13 
13 
     | 
    
         
             
            - Sean Grove
         
     | 
| 
         @@ -17,7 +17,7 @@ autorequire: 
     | 
|
| 
       17 
17 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       18 
18 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
     | 
    
         
            -
            date: 2011-01- 
     | 
| 
      
 20 
     | 
    
         
            +
            date: 2011-01-21 00:00:00 -03:00
         
     | 
| 
       21 
21 
     | 
    
         
             
            default_executable: sauce
         
     | 
| 
       22 
22 
     | 
    
         
             
            dependencies: 
         
     | 
| 
       23 
23 
     | 
    
         
             
            - !ruby/object:Gem::Dependency 
         
     |