Skip to content

Commit

Permalink
Merge pull request #442 from pvande/tickets/2.7.x/12339-module-search…
Browse files Browse the repository at this point in the history
…-formatting

Tickets/2.7.x/12339 module search formatting
  • Loading branch information
slippycheeze committed Feb 9, 2012
2 parents cce6dbf + bfcb775 commit 43ff7f3
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 13 deletions.
60 changes: 47 additions & 13 deletions lib/puppet/face/module/search.rb
@@ -1,3 +1,5 @@
require 'puppet/util/terminal'

Puppet::Face.define(:module, '1.0.0') do
action(:search) do
summary "Search a repository for a module."
Expand Down Expand Up @@ -29,27 +31,59 @@
Puppet::Module::Tool::Applications::Searcher.run(term, options)
end

when_rendering :console do |return_value|
when_rendering :console do |results, term, options|
return '' if results.empty?

FORMAT = "%-10s %-32s %-14s %s\n"
padding = ' '
headers = {
'full_name' => 'NAME',
'desc' => 'DESCRIPTION',
'author' => 'AUTHOR',
'tag_list' => 'KEYWORDS',
}

def header
FORMAT % ['NAME', 'DESCRIPTION', 'AUTHOR', 'KEYWORDS']
end
min_widths = Hash[headers.map { |k,v| [k, v.length] }]
min_widths['full_name'] = min_widths['author'] = 12

min_width = min_widths.inject(0) { |sum,pair| sum += pair.last } + (padding.length * (headers.length - 1))

def format_row(name, description, author, tag_list)
keywords = tag_list.join(' ')
FORMAT % [name[0..10], description[0..32], "@#{author[0..14]}", keywords]
terminal_width = [Puppet::Util::Terminal.width, min_width].max

columns = results.inject(min_widths) do |hash, result|
{
'full_name' => [ hash['full_name'], result['full_name'].length ].max,
'desc' => [ hash['desc'], result['desc'].length ].max,
'author' => [ hash['author'], result['author'].length ].max,
'tag_list' => [ hash['tag_list'], result['tag_list'].join(' ').length ].max,
}
end

output = ''
output << header unless return_value.empty?
flex_width = terminal_width - columns['full_name'] - columns['author'] - (padding.length * (headers.length - 1))
tag_lists = results.map { |r| r['tag_list'] }

return_value.map do |match|
output << format_row(match['name'], match['desc'], match['author'], match['tag_list'])
while (columns['tag_list'] > flex_width / 3)
longest_tag_list = tag_lists.max_by { |tl| tl.join(' ').length }
break if [ [], [term] ].include? longest_tag_list
longest_tag_list.delete(longest_tag_list.max_by { |t| t == term ? -1 : t.length })
columns['tag_list'] = tag_lists.map { |tl| tl.join(' ').length }.max
end

output
columns['tag_list'] = [
flex_width / 3,
tag_lists.map { |tl| tl.join(' ').length }.max,
].max
columns['desc'] = flex_width - columns['tag_list']

format = %w{full_name desc author tag_list}.map do |k|
"%-#{ [ columns[k], min_widths[k] ].max }s"
end.join(padding) + "\n"

format % [ headers['full_name'], headers['desc'], headers['author'], headers['tag_list'] ] +
results.map do |match|
name, desc, author, keywords = %w{full_name desc author tag_list}.map { |k| match[k] }
desc = desc[0...(columns['desc'] - 3)] + '...' if desc.length > columns['desc']
format % [name.sub('/', '-'), desc, "@#{author}", [keywords].flatten.join(' ')]
end.join
end
end
end
17 changes: 17 additions & 0 deletions lib/puppet/util/terminal.rb
@@ -0,0 +1,17 @@
module Puppet::Util::Terminal
# Attempts to determine the width of the terminal. This is currently only
# supported on POSIX systems, and relies on the claims of `stty` (or `tput`).
#
# Inspired by code from Thor; thanks wycats!
# @return [Number] The column width of the terminal. Defaults to 80 columns.
def self.width
if Puppet.features.posix?
result = %x{stty size 2>/dev/null}.split[1] ||
%x{tput cols 2>/dev/null}.split[0] ||
'80'
end
return result.to_i
rescue
return 80
end
end
131 changes: 131 additions & 0 deletions spec/unit/face/module/search_spec.rb
@@ -1,5 +1,7 @@
require 'spec_helper'
require 'puppet/face'
require 'puppet/application/module'
require 'puppet/module_tool'

describe "puppet module search" do
subject { Puppet::Face[:module, :current] }
Expand All @@ -8,6 +10,135 @@
{}
end

describe Puppet::Application::Module do
subject do
app = Puppet::Application::Module.new
app.stubs(:action).returns(Puppet::Face.find_action(:module, :search))
app
end

before { subject.render_as = :console }
before { Puppet::Util::Terminal.stubs(:width).returns(100) }

it 'should output nothing when receiving an empty dataset' do
subject.render([], ['apache', {}]).should == ''
end

it 'should output a header when receiving a non-empty dataset' do
results = [
{'full_name' => '', 'author' => '', 'desc' => '', 'tag_list' => [] },
]

subject.render(results, ['apache', {}]).should =~ /NAME/
subject.render(results, ['apache', {}]).should =~ /DESCRIPTION/
subject.render(results, ['apache', {}]).should =~ /AUTHOR/
subject.render(results, ['apache', {}]).should =~ /KEYWORDS/
end

it 'should output the relevant fields when receiving a non-empty dataset' do
results = [
{'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Summary', 'tag_list' => ['tag1', 'tag2'] },
]

subject.render(results, ['apache', {}]).should =~ /Name/
subject.render(results, ['apache', {}]).should =~ /Author/
subject.render(results, ['apache', {}]).should =~ /Summary/
subject.render(results, ['apache', {}]).should =~ /tag1/
subject.render(results, ['apache', {}]).should =~ /tag2/
end

it 'should elide really long descriptions' do
results = [
{
'full_name' => 'Name',
'author' => 'Author',
'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it',
'tag_list' => ['tag1', 'tag2'],
},
]

subject.render(results, ['apache', {}]).should =~ /\.{3} @Author/
end

it 'should never truncate the module name' do
results = [
{
'full_name' => 'This-module-has-a-really-really-long-name',
'author' => 'Author',
'desc' => 'Description',
'tag_list' => ['tag1', 'tag2'],
},
]

subject.render(results, ['apache', {}]).should =~ /This-module-has-a-really-really-long-name/
end

it 'should never truncate the author name' do
results = [
{
'full_name' => 'Name',
'author' => 'This-author-has-a-really-really-long-name',
'desc' => 'Description',
'tag_list' => ['tag1', 'tag2'],
},
]

subject.render(results, ['apache', {}]).should =~ /@This-author-has-a-really-really-long-name/
end

it 'should never remove tags that match the search term' do
results = [
{
'full_name' => 'Name',
'author' => 'Author',
'desc' => 'Description',
'tag_list' => ['Supercalifragilisticexpialidocious'] + (1..100).map { |i| "tag#{i}" },
},
]

subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should =~ /Supercalifragilisticexpialidocious/
subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should_not =~ /tag/
end

{
100 => <<-EOT,
NAME DESCRIPTION AUTHOR KEYWORDS
Name This description is really too long to fit i... @Author tag1 tag2 taggitty3
EOT

70 => <<-EOT,
NAME DESCRIPTION AUTHOR KEYWORDS
Name This description is real... @Author tag1 tag2
EOT

80 => <<-EOT,
NAME DESCRIPTION AUTHOR KEYWORDS
Name This description is really too ... @Author tag1 tag2
EOT

200 => <<-EOT,
NAME DESCRIPTION AUTHOR KEYWORDS
Name This description is really too long to fit in a single data table, guys -- we should probably set about truncat... @Author tag1 tag2 taggitty3
EOT
}.each do |width, expectation|
it 'should resize the table to fit the screen, when #{width} columns' do
results = [
{
'full_name' => 'Name',
'author' => 'Author',
'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it',
'tag_list' => ['tag1', 'tag2', 'taggitty3'],
},
]

Puppet::Util::Terminal.expects(:width).returns(width)
result = subject.render(results, ['apache', {}])
result.lines.max_by(&:length).chomp.length.should <= width
result.should == expectation
end
end
end

describe "option validation" do
context "without any options" do
it "should require a search term" do
Expand Down
34 changes: 34 additions & 0 deletions spec/unit/util/terminal_spec.rb
@@ -0,0 +1,34 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'puppet/util/terminal'

describe Puppet::Util::Terminal do
describe '.width' do
it 'should invoke `stty` and return the width' do
height, width = 100, 200
subject.expects(:`).with('stty size 2>/dev/null').returns("#{height} #{width}\n")
subject.width.should == width
end

it 'should use `tput` if `stty` is unavailable' do
width = 200
subject.expects(:`).with('stty size 2>/dev/null').returns("\n")
subject.expects(:`).with('tput cols 2>/dev/null').returns("#{width}\n")
subject.width.should == width
end

it 'should default to 80 columns if `tput` and `stty` are unavailable' do
width = 80
subject.expects(:`).with('stty size 2>/dev/null').returns("\n")
subject.expects(:`).with('tput cols 2>/dev/null').returns("\n")
subject.width.should == width
end

it 'should default to 80 columns if `tput` or `stty` raise exceptions' do
width = 80
subject.expects(:`).with('stty size 2>/dev/null').raises()
subject.stubs(:`).with('tput cols 2>/dev/null').returns("#{width + 1000}\n")
subject.width.should == width
end
end
end

0 comments on commit 43ff7f3

Please sign in to comment.