123456789_123456789_123456789_123456789_123456789_

Module: SimpleCov::CLI::Serve

Relationships & Source Files
Defined in: lib/simplecov/cli/serve.rb

Overview

simplecov serve [--port N] [--host HOST] — serve the coverage report over HTTP. A 30-line static file server backed by stdlib socket, so there's no extra dependency just for "view a local report on a CI box where file:// doesn't work."

Constant Summary

  • MIME =
    # File 'lib/simplecov/cli/serve.rb', line 12
    {
      ".html" => "text/html; charset=utf-8",
      ".htm" => "text/html; charset=utf-8",
      ".css" => "text/css",
      ".js" => "application/javascript",
      ".json" => "application/json",
      ".svg" => "image/svg+xml",
      ".png" => "image/png",
      ".gif" => "image/gif",
      ".jpg" => "image/jpeg",
      ".jpeg" => "image/jpeg",
      ".ico" => "image/x-icon",
      ".txt" => "text/plain; charset=utf-8"
    }.freeze
  • STATUS_TEXT =
    # File 'lib/simplecov/cli/serve.rb', line 26
    {
      200 => "OK", 400 => "Bad Request", 403 => "Forbidden",
      404 => "Not Found", 405 => "Method Not Allowed"
    }.freeze

Class Method Summary

Class Method Details

.announce(stdout, server, dir) (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 56

def announce(stdout, server, dir)
  port = server.addr[1]
  host = server.addr[3]
  stdout.puts("simplecov serve: serving #{dir} at http://#{host}:#{port}/")
  stdout.puts("Press Ctrl-C to stop.")
end

.drain_headers(client) (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 92

def drain_headers(client)
  loop { break if client.readline.strip.empty? }
end

.error(stderr, message) (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 133

def error(stderr, message)
  stderr.puts("simplecov serve: #{message}")
  1
end

.handle_connection(client, root) (mod_func)

Reads one HTTP request line, drains headers, serves the file or writes a status response. Wide rescue so a misbehaving client can't crash the server.

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 72

def handle_connection(client, root)
  method, path = client.readline.split
  drain_headers(client)
  return respond(client, 405) unless method == "GET"

  file = resolve(path, root)
  return respond(client, file == :forbidden ? 403 : 404) unless file.is_a?(String)

  respond(client, 200, File.binread(file), MIME[File.extname(file).downcase])
rescue StandardError
  # Misbehaving clients (truncated requests, connection resets,
  # invalid encoding) shouldn't take the whole server down.
  nil
ensure
  # simplecov:disable — `client` is the parameter, never nil here;
  # the `&.` is purely defensive in case of future refactors
  client&.close
  # simplecov:enable
end

.inside?(path, root) ⇒ Boolean (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 121

def inside?(path, root)
  path == root || path.start_with?(root + File::SEPARATOR)
end

.parse(args) (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 47

def parse(args)
  opts = {port: 0, host: "127.0.0.1"}
  OptionParser.new do |o|
    o.on("--port N", Integer) { |v| opts[:port] = v }
    o.on("--host HOST")       { |v| opts[:host] = v }
  end.parse(args)
  opts
end

.resolve(request_path, root) (mod_func)

Returns the absolute path of the file to serve, :forbidden for a traversal attempt (including symlinks that escape root), or nil for "not found".

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 99

def resolve(request_path, root)
  path = request_path.split("?", 2).first.to_s.sub(%r{^/}, "")
  absolute_root = File.realpath(root)
  candidate = File.expand_path(path.empty? ? "index.html" : path, absolute_root)
  # Reject `..` traversal and absolute-path attempts before
  # touching disk so they're 403, not 404.
  return :forbidden unless inside?(candidate, absolute_root)

  candidate = File.join(candidate, "index.html") if File.directory?(candidate)
  return nil unless File.file?(candidate)

  # Resolve symlinks last and re-check: a file inside root could
  # be a symlink pointing outside (e.g. /etc/passwd).
  real = File.realpath(candidate)
  inside?(real, absolute_root) ? real : :forbidden
rescue Errno::ENOENT
  # simplecov:disable — TOCTOU: candidate vanished between
  # File.file? and File.realpath. Treat as "not found".
  nil
  # simplecov:enable
end

.respond(client, status, body = "", content_type = "text/plain") (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 125

def respond(client, status, body = "", content_type = "text/plain")
  client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] || 'Error'}\r\n",
               "Content-Type: #{content_type || 'application/octet-stream'}\r\n",
               "Content-Length: #{body.bytesize}\r\n",
               "Connection: close\r\n\r\n")
  client.write(body)
end

.run(args, stdout:, stderr:) (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 33

def run(args, stdout:, stderr:, **)
  opts = parse(args)
  dir = SimpleCov::CLI.coverage_dir
  return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir)

  require "socket"
  server = TCPServer.new(opts[:host], opts[:port])
  announce(stdout, server, dir)
  serve_loop(server, dir, stdout)
  0
ensure
  server&.close
end

.serve_loop(server, dir, stdout) (mod_func)

[ GitHub ]

  
# File 'lib/simplecov/cli/serve.rb', line 63

def serve_loop(server, dir, stdout)
  loop { handle_connection(server.accept, dir) }
rescue Interrupt
  stdout.puts("\nsimplecov serve: stopping")
end