Use cache_store for descriptions

This makes use of both the existing interfaces and could use the
existing cache file but we'll create a new one and cleanup the old one
to avoid issues and use a more consistent name.
This commit is contained in:
Mike McQuaid 2018-10-13 08:22:51 -07:00
parent 17154abf46
commit fe6b78a3f3
No known key found for this signature in database
GPG Key ID: 48A898132FD8EE70
15 changed files with 230 additions and 145 deletions

View File

@ -49,6 +49,28 @@ class CacheStoreDatabase
cache_path.exist? cache_path.exist?
end end
# Returns the modification time of the cache file (if it already exists).
#
# @return [Time]
def mtime
return unless created?
cache_path.mtime
end
# Performs a `select` on the underlying database.
#
# @return [Array]
def select(&block)
db.select(&block)
end
# Returns `true` if the cache is empty.
#
# @return [Boolean]
def empty?
db.empty?
end
private private
# Lazily loaded database in read/write mode. If this method is called, a # Lazily loaded database in read/write mode. If this method is called, a
@ -105,14 +127,14 @@ class CacheStore
# stored # stored
# #
# @abstract # @abstract
def fetch_type(*) def fetch(*)
raise NotImplementedError raise NotImplementedError
end end
# Deletes data from the cache based on a condition defined in a concrete class # Deletes data from the cache based on a condition defined in a concrete class
# #
# @abstract # @abstract
def flush_cache! def delete!(*)
raise NotImplementedError raise NotImplementedError
end end

View File

@ -161,7 +161,7 @@ module Homebrew
cleanup_portable_ruby cleanup_portable_ruby
return if dry_run? return if dry_run?
cleanup_linkage_db cleanup_old_cache_db
rm_ds_store rm_ds_store
else else
args.each do |arg| args.each do |arg|
@ -325,8 +325,12 @@ module Homebrew
end end
end end
def cleanup_linkage_db def cleanup_old_cache_db
FileUtils.rm_rf [cache/"linkage.db", cache/"linkage.db.db"] FileUtils.rm_rf [
cache/"desc_cache.json",
cache/"linkage.db",
cache/"linkage.db.db",
]
end end
def rm_ds_store(dirs = nil) def rm_ds_store(dirs = nil)

View File

@ -10,6 +10,7 @@
require "descriptions" require "descriptions"
require "search" require "search"
require "description_cache_store"
module Homebrew module Homebrew
module_function module_function
@ -21,23 +22,27 @@ module Homebrew
search_type << :either if ARGV.flag? "--search" search_type << :either if ARGV.flag? "--search"
search_type << :name if ARGV.flag? "--name" search_type << :name if ARGV.flag? "--name"
search_type << :desc if ARGV.flag? "--description" search_type << :desc if ARGV.flag? "--description"
if search_type.size > 1
odie "Pick one, and only one, of -s/--search, -n/--name, or -d/--description."
elsif search_type.present? && ARGV.named.empty?
odie "You must provide a search term."
end
if search_type.empty? results = if search_type.empty?
raise FormulaUnspecifiedError if ARGV.named.empty? raise FormulaUnspecifiedError if ARGV.named.empty?
desc = {} desc = {}
ARGV.formulae.each { |f| desc[f.full_name] = f.desc } ARGV.formulae.each { |f| desc[f.full_name] = f.desc }
results = Descriptions.new(desc) Descriptions.new(desc)
results.print else
elsif search_type.size > 1
odie "Pick one, and only one, of -s/--search, -n/--name, or -d/--description."
elsif !ARGV.named.empty?
arg = ARGV.named.join(" ") arg = ARGV.named.join(" ")
string_or_regex = query_regexp(arg) string_or_regex = query_regexp(arg)
results = Descriptions.search(string_or_regex, search_type.first) CacheStoreDatabase.use(:descriptions) do |db|
results.print cache_store = DescriptionCacheStore.new(db)
else Descriptions.search(string_or_regex, search_type.first, cache_store)
odie "You must provide a search term." end
end end
results.print
end end
end end

View File

@ -8,6 +8,7 @@ require "formulary"
require "descriptions" require "descriptions"
require "cleanup" require "cleanup"
require "update_migrator" require "update_migrator"
require "description_cache_store"
module Homebrew module Homebrew
module_function module_function
@ -127,7 +128,10 @@ module Homebrew
hub.dump hub.dump
hub.reporters.each(&:migrate_tap_migration) hub.reporters.each(&:migrate_tap_migration)
hub.reporters.each(&:migrate_formula_rename) hub.reporters.each(&:migrate_formula_rename)
Descriptions.update_cache(hub) CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.update_from_report!(hub)
end
end end
puts if ARGV.include?("--preinstall") puts if ARGV.include?("--preinstall")
end end

View File

@ -0,0 +1,88 @@
require "set"
require "cache_store"
require "searchable"
#
# `DescriptionCacheStore` provides methods to fetch and mutate linkage-specific data used
# by the `brew linkage` command
#
class DescriptionCacheStore < CacheStore
include Searchable
# Inserts a formula description into the cache if it does not exist or
# updates the formula description if it does exist
#
# @param [String] formula_name: the name of the formula to set
# @param [String] description: the description from the formula to set
# @return [nil]
def update!(formula_name, description)
database.set(formula_name, description)
end
# Delete the formula description from the `DescriptionCacheStore`
#
# @param [String] formula_name: the name of the formula to delete
# @return [nil]
def delete!(formula_name)
database.delete(formula_name)
end
# If the database is empty `update!` it with all known formulae.
# @return [nil]
def populate_if_empty!
return unless database.empty?
Formula.each { |f| update!(f.full_name, f.desc) }
end
# Use an update report to update the `DescriptionCacheStore`.
#
# @param [Report] report: an update report generated by cmd/update.rb
# @return [nil]
def update_from_report!(report)
return if report.empty?
renamings = report.select_formula(:R)
alterations = report.select_formula(:A) +
report.select_formula(:M) +
renamings.map(&:last)
update_from_formula_names!(alterations)
delete_from_formula_names!(report.select_formula(:D) +
renamings.map(&:first))
end
# Use an array of formulae names to update the `DescriptionCacheStore`.
#
# @param [Array] formula_names: the formulae to update.
# @return [nil]
def update_from_formula_names!(formula_names)
formula_names.each do |name|
begin
update!(name, Formula[name].desc)
rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS => e
p e
delete!(name)
end
end
end
# Use an array of formulae names to delete them from the `DescriptionCacheStore`.
#
# @param [Array] formula_names: the formulae to delete.
# @return [nil]
def delete_from_formula_names!(formula_names)
formula_names.each(&method(:delete!))
end
private
# Not implemented; access is through `Searchable`.
def fetch
super
end
# `select` from the underlying database.
def select(&block)
database.select(&block)
end
end

View File

@ -6,113 +6,17 @@ require "searchable"
class Descriptions class Descriptions
extend Homebrew::Search extend Homebrew::Search
CACHE_FILE = HOMEBREW_CACHE + "desc_cache.json"
def self.cache
@cache || load_cache
end
# If the cache file exists, load it into, and return, a hash; otherwise,
# return nil.
def self.load_cache
@cache = JSON.parse(CACHE_FILE.read) if CACHE_FILE.exist?
end
# Write the cache to disk after ensuring the existence of the containing
# directory.
def self.save_cache
HOMEBREW_CACHE.mkpath
CACHE_FILE.atomic_write JSON.dump(@cache)
end
# Create a hash mapping all formulae to their descriptions;
# save it for future use.
def self.generate_cache
@cache = {}
Formula.each do |f|
@cache[f.full_name] = f.desc
end
save_cache
end
# Return true if the cache exists, and none of the Taps
# repos were updated more recently than it was.
def self.cache_fresh?
return false unless CACHE_FILE.exist?
cache_mtime = File.mtime(CACHE_FILE)
Tap.each do |tap|
next unless tap.git?
repo_mtime = File.mtime(tap.path/".git/refs/heads/master")
return false if repo_mtime > cache_mtime
end
true
end
# Create the cache if it doesn't already exist.
def self.ensure_cache
generate_cache unless cache_fresh? && cache
end
# Take a {Report}, as generated by cmd/update.rb.
# Unless the cache file exists, do nothing.
# If it does exist, but the Report is empty, just touch the cache file.
# Otherwise, use the report to update the cache.
def self.update_cache(report)
return unless CACHE_FILE.exist?
if report.empty?
FileUtils.touch CACHE_FILE
else
renamings = report.select_formula(:R)
alterations = report.select_formula(:A) + report.select_formula(:M) +
renamings.map(&:last)
cache_formulae(alterations, save: false)
uncache_formulae(report.select_formula(:D) +
renamings.map(&:first))
end
end
# Given an array of formula names, add them and their descriptions to the
# cache. Save the updated cache to disk, unless explicitly told not to.
def self.cache_formulae(formula_names, options = { save: true })
return unless cache
formula_names.each do |name|
begin
@cache[name] = Formulary.factory(name).desc
rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS
@cache.delete(name)
end
end
save_cache if options[:save]
end
# Given an array of formula names, remove them and their descriptions from
# the cache. Save the updated cache to disk, unless explicitly told not to.
def self.uncache_formulae(formula_names, options = { save: true })
return unless cache
formula_names.each { |name| @cache.delete(name) }
save_cache if options[:save]
end
# Given a regex, find all formulae whose specified fields contain a match. # Given a regex, find all formulae whose specified fields contain a match.
def self.search(string_or_regex, field = :either) def self.search(string_or_regex, field, cache_store)
ensure_cache cache_store.populate_if_empty!
@cache.extend(Searchable)
results = case field results = case field
when :name when :name
@cache.search(string_or_regex) { |name, _| name } cache_store.search(string_or_regex) { |name, _| name }
when :desc when :desc
@cache.search(string_or_regex) { |_, desc| desc } cache_store.search(string_or_regex) { |_, desc| desc }
when :either when :either
@cache.search(string_or_regex) cache_store.search(string_or_regex)
end end
new(results) new(results)
@ -129,7 +33,11 @@ class Descriptions
blank = Formatter.warning("[no description]") blank = Formatter.warning("[no description]")
@descriptions.keys.sort.each do |full_name| @descriptions.keys.sort.each do |full_name|
short_name = short_names[full_name] short_name = short_names[full_name]
printed_name = (short_name_counts[short_name] == 1) ? short_name : full_name printed_name = if short_name_counts[short_name] == 1
short_name
else
full_name
end
description = @descriptions[full_name] || blank description = @descriptions[full_name] || blank
puts "#{Tty.bold}#{printed_name}:#{Tty.reset} #{description}" puts "#{Tty.bold}#{printed_name}:#{Tty.reset} #{description}"
end end
@ -143,6 +51,9 @@ class Descriptions
def short_name_counts def short_name_counts
@short_name_counts ||= @short_name_counts ||=
short_names.values.each_with_object(Hash.new(0)) { |name, counts| counts[name] += 1 } short_names.values
.each_with_object(Hash.new(0)) do |name, counts|
counts[name] += 1
end
end end
end end

View File

@ -303,7 +303,7 @@ class Keg
CacheStoreDatabase.use(:linkage) do |db| CacheStoreDatabase.use(:linkage) do |db|
break unless db.created? break unless db.created?
LinkageCacheStore.new(path, db).flush_cache! LinkageCacheStore.new(path, db).delete!
end end
path.rmtree path.rmtree

View File

@ -41,7 +41,7 @@ class LinkageCacheStore < CacheStore
# @param [Symbol] the type to fetch from the `LinkageCacheStore` # @param [Symbol] the type to fetch from the `LinkageCacheStore`
# @raise [TypeError] error if the type is not in `HASH_LINKAGE_TYPES` # @raise [TypeError] error if the type is not in `HASH_LINKAGE_TYPES`
# @return [Hash] # @return [Hash]
def fetch_type(type) def fetch(type)
unless HASH_LINKAGE_TYPES.include?(type) unless HASH_LINKAGE_TYPES.include?(type)
raise TypeError, <<~EOS raise TypeError, <<~EOS
Can't fetch types that are not defined for the linkage store Can't fetch types that are not defined for the linkage store
@ -53,8 +53,10 @@ class LinkageCacheStore < CacheStore
fetch_hash_values(type) fetch_hash_values(type)
end end
# Delete the keg from the `LinkageCacheStore`
#
# @return [nil] # @return [nil]
def flush_cache! def delete!
database.delete(@keg_path) database.delete(@keg_path)
end end

View File

@ -79,9 +79,9 @@ class LinkageChecker
keg_files_dylibs = nil keg_files_dylibs = nil
if rebuild_cache if rebuild_cache
store&.flush_cache! store&.delete!
else else
keg_files_dylibs = store&.fetch_type(:keg_files_dylibs) keg_files_dylibs = store&.fetch(:keg_files_dylibs)
end end
keg_files_dylibs_was_empty = false keg_files_dylibs_was_empty = false

View File

@ -1,4 +1,5 @@
require "searchable" require "searchable"
require "description_cache_store"
module Homebrew module Homebrew
module Search module Search
@ -14,7 +15,10 @@ module Homebrew
def search_descriptions(string_or_regex) def search_descriptions(string_or_regex)
ohai "Formulae" ohai "Formulae"
Descriptions.search(string_or_regex, :desc).print CacheStoreDatabase.use(:descriptions) do |db|
cache_store = DescriptionCacheStore.new(db)
Descriptions.search(string_or_regex, :desc, cache_store).print
end
end end
def search_taps(query, silent: false) def search_taps(query, silent: false)

View File

@ -1,5 +1,6 @@
require "extend/cachable" require "extend/cachable"
require "readall" require "readall"
require "description_cache_store"
# a {Tap} is used to extend the formulae provided by Homebrew core. # a {Tap} is used to extend the formulae provided by Homebrew core.
# Usually, it's synced with a remote git repository. And it's likely # Usually, it's synced with a remote git repository. And it's likely
@ -299,7 +300,10 @@ class Tap
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ") formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet
Descriptions.cache_formulae(formula_names) CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.update_from_formula_names!(formula_names)
end
return if options[:clone_target] return if options[:clone_target]
return unless private? return unless private?
@ -331,7 +335,10 @@ class Tap
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ") formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
unpin if pinned? unpin if pinned?
Descriptions.uncache_formulae(formula_names) CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.delete_from_formula_names!(formula_names)
end
Utils::Link.unlink_manpages(path) Utils::Link.unlink_manpages(path)
Utils::Link.unlink_completions(path) Utils::Link.unlink_completions(path)
path.rmtree path.rmtree

View File

@ -1,6 +1,4 @@
describe "brew desc", :integration_test do describe "brew desc", :integration_test do
let(:desc_cache) { HOMEBREW_CACHE/"desc_cache.json" }
it "shows a given Formula's description" do it "shows a given Formula's description" do
setup_test_formula "testball" setup_test_formula "testball"
@ -9,14 +7,4 @@ describe "brew desc", :integration_test do
.and not_to_output.to_stderr .and not_to_output.to_stderr
.and be_a_success .and be_a_success
end end
describe "--description" do
it "creates a description cache" do
expect(desc_cache).not_to exist
expect { brew "desc", "--description", "testball" }.to be_a_success
expect(desc_cache).to exist
end
end
end end

View File

@ -34,7 +34,7 @@ describe "brew search", :integration_test do
end end
describe "--desc" do describe "--desc" do
let(:desc_cache) { HOMEBREW_CACHE/"desc_cache.json" } let(:desc_cache) { HOMEBREW_CACHE/"descriptions.json" }
it "supports searching in descriptions and creates a description cache" do it "supports searching in descriptions and creates a description cache" do
expect(desc_cache).not_to exist expect(desc_cache).not_to exist

View File

@ -0,0 +1,50 @@
require "description_cache_store"
describe DescriptionCacheStore do
subject(:cache_store) { described_class.new(database) }
let(:database) { double("database") }
let(:formula_name) { "test_name" }
let(:description) { "test_description" }
describe "#update!" do
it "sets the formula description" do
expect(database).to receive(:set).with(formula_name, description)
cache_store.update!(formula_name, description)
end
end
describe "#delete!" do
it "deletes the formula description" do
expect(database).to receive(:delete).with(formula_name)
cache_store.delete!(formula_name)
end
end
describe "#update_from_report!" do
let(:report) { double(select_formula: [], empty?: false) }
it "reads from the report" do
cache_store.update_from_report!(report)
end
end
describe "#update_from_formula_names!" do
it "sets the formulae descriptions" do
f = formula do
url "url-1"
desc "desc"
end
expect(Formulary).to receive(:factory).with(f.name).and_return(f)
expect(database).to receive(:set).with(f.name, f.desc)
cache_store.update_from_formula_names!([f.name])
end
end
describe "#delete_from_formula_names!" do
it "deletes the formulae descriptions" do
expect(database).to receive(:delete).with(formula_name)
cache_store.delete_from_formula_names!([formula_name])
end
end
end

View File

@ -43,27 +43,27 @@ describe LinkageCacheStore do
end end
end end
describe "#flush_cache!" do describe "#delete!" do
it "calls `delete` on the `database` with `keg_name` as parameter" do it "calls `delete` on the `database` with `keg_name` as parameter" do
expect(database).to receive(:delete).with(keg_name) expect(database).to receive(:delete).with(keg_name)
subject.flush_cache! subject.delete!
end end
end end
describe "#fetch_type" do describe "#fetch" do
context "`HASH_LINKAGE_TYPES.include?(type)`" do context "`HASH_LINKAGE_TYPES.include?(type)`" do
before do before do
expect(database).to receive(:get).with(keg_name).and_return(nil) expect(database).to receive(:get).with(keg_name).and_return(nil)
end end
it "returns a `Hash` of values" do it "returns a `Hash` of values" do
expect(subject.fetch_type(:keg_files_dylibs)).to be_an_instance_of(Hash) expect(subject.fetch(:keg_files_dylibs)).to be_an_instance_of(Hash)
end end
end end
context "`type` not in `HASH_LINKAGE_TYPES`" do context "`type` not in `HASH_LINKAGE_TYPES`" do
it "raises a `TypeError` if the `type` is not supported" do it "raises a `TypeError` if the `type` is not supported" do
expect { subject.fetch_type(:bad_type) }.to raise_error(TypeError) expect { subject.fetch(:bad_type) }.to raise_error(TypeError)
end end
end end
end end