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?
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
# Lazily loaded database in read/write mode. If this method is called, a
@ -105,14 +127,14 @@ class CacheStore
# stored
#
# @abstract
def fetch_type(*)
def fetch(*)
raise NotImplementedError
end
# Deletes data from the cache based on a condition defined in a concrete class
#
# @abstract
def flush_cache!
def delete!(*)
raise NotImplementedError
end

View File

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

View File

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

View File

@ -8,6 +8,7 @@ require "formulary"
require "descriptions"
require "cleanup"
require "update_migrator"
require "description_cache_store"
module Homebrew
module_function
@ -127,7 +128,10 @@ module Homebrew
hub.dump
hub.reporters.each(&:migrate_tap_migration)
hub.reporters.each(&:migrate_formula_rename)
Descriptions.update_cache(hub)
CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.update_from_report!(hub)
end
end
puts if ARGV.include?("--preinstall")
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
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.
def self.search(string_or_regex, field = :either)
ensure_cache
@cache.extend(Searchable)
def self.search(string_or_regex, field, cache_store)
cache_store.populate_if_empty!
results = case field
when :name
@cache.search(string_or_regex) { |name, _| name }
cache_store.search(string_or_regex) { |name, _| name }
when :desc
@cache.search(string_or_regex) { |_, desc| desc }
cache_store.search(string_or_regex) { |_, desc| desc }
when :either
@cache.search(string_or_regex)
cache_store.search(string_or_regex)
end
new(results)
@ -129,7 +33,11 @@ class Descriptions
blank = Formatter.warning("[no description]")
@descriptions.keys.sort.each do |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
puts "#{Tty.bold}#{printed_name}:#{Tty.reset} #{description}"
end
@ -143,6 +51,9 @@ class Descriptions
def 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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
require "searchable"
require "description_cache_store"
module Homebrew
module Search
@ -14,7 +15,10 @@ module Homebrew
def search_descriptions(string_or_regex)
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
def search_taps(query, silent: false)

View File

@ -1,5 +1,6 @@
require "extend/cachable"
require "readall"
require "description_cache_store"
# 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
@ -299,7 +300,10 @@ class Tap
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
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 unless private?
@ -331,7 +335,10 @@ class Tap
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
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_completions(path)
path.rmtree

View File

@ -1,6 +1,4 @@
describe "brew desc", :integration_test do
let(:desc_cache) { HOMEBREW_CACHE/"desc_cache.json" }
it "shows a given Formula's description" do
setup_test_formula "testball"
@ -9,14 +7,4 @@ describe "brew desc", :integration_test do
.and not_to_output.to_stderr
.and be_a_success
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

View File

@ -34,7 +34,7 @@ describe "brew search", :integration_test do
end
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
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
describe "#flush_cache!" do
describe "#delete!" do
it "calls `delete` on the `database` with `keg_name` as parameter" do
expect(database).to receive(:delete).with(keg_name)
subject.flush_cache!
subject.delete!
end
end
describe "#fetch_type" do
describe "#fetch" do
context "`HASH_LINKAGE_TYPES.include?(type)`" do
before do
expect(database).to receive(:get).with(keg_name).and_return(nil)
end
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
context "`type` not in `HASH_LINKAGE_TYPES`" 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